Commit fc628627 authored by Mark Karpilovskij's avatar Mark Karpilovskij Committed by Daniel Salzman

queryacl: new module for query access control

parent 1d795f51
......@@ -153,6 +153,7 @@ src/knot/modules/noudp/noudp.c
src/knot/modules/onlinesign/nsec_next.c
src/knot/modules/onlinesign/nsec_next.h
src/knot/modules/onlinesign/onlinesign.c
src/knot/modules/queryacl/queryacl.c
src/knot/modules/rrl/functions.c
src/knot/modules/rrl/functions.h
src/knot/modules/rrl/rrl.c
......
......@@ -327,6 +327,7 @@ KNOT_MODULE([dnstap], "no")
KNOT_MODULE([geoip], "yes")
KNOT_MODULE([noudp], "yes")
KNOT_MODULE([onlinesign], "yes", "non-shareable")
KNOT_MODULE([queryacl], "yes")
KNOT_MODULE([rrl], "yes")
KNOT_MODULE([stats], "yes")
KNOT_MODULE([synthrecord], "yes")
......
......@@ -83,6 +83,8 @@ The ``default`` template identifier is reserved for the default template::
.. NOTE::
Each template option can be explicitly overridden in zone-specific configuration.
.. _ACL:
Access control list (ACL)
=========================
......
......@@ -191,6 +191,7 @@ include $(srcdir)/knot/modules/dnstap/Makefile.inc
include $(srcdir)/knot/modules/geoip/Makefile.inc
include $(srcdir)/knot/modules/noudp/Makefile.inc
include $(srcdir)/knot/modules/onlinesign/Makefile.inc
include $(srcdir)/knot/modules/queryacl/Makefile.inc
include $(srcdir)/knot/modules/rrl/Makefile.inc
include $(srcdir)/knot/modules/stats/Makefile.inc
include $(srcdir)/knot/modules/synthrecord/Makefile.inc
......
knot_modules_queryacl_la_SOURCES = knot/modules/queryacl/queryacl.c \
EXTRA_DIST += knot/modules/queryacl/queryacl.rst
if STATIC_MODULE_queryacl
libknotd_la_SOURCES += $(knot_modules_queryacl_la_SOURCES)
endif
if SHARED_MODULE_queryacl
knot_modules_queryacl_la_LDFLAGS = $(KNOTD_MOD_LDFLAGS)
knot_modules_queryacl_la_CPPFLAGS = $(KNOTD_MOD_CPPFLAGS)
pkglib_LTLIBRARIES += knot/modules/queryacl.la
endif
/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "knot/include/module.h"
#include "contrib/sockaddr.h"
#define MOD_ADDRESS "\x07""address"
#define MOD_INTERFACE "\x09""interface"
const yp_item_t queryacl_conf[] = {
{ MOD_ADDRESS, YP_TNET, YP_VNONE, YP_FMULTI },
{ MOD_INTERFACE, YP_TNET, YP_VNONE, YP_FMULTI },
{ NULL }
};
typedef struct {
knotd_conf_t allow_addr;
knotd_conf_t allow_iface;
} queryacl_ctx_t;
static knotd_state_t queryacl_process(knotd_state_t state, knot_pkt_t *pkt,
knotd_qdata_t *qdata, knotd_mod_t *mod)
{
assert(pkt && qdata && mod);
queryacl_ctx_t *ctx = knotd_mod_ctx(mod);
// Continue only for regular queries.
if (qdata->type != KNOTD_QUERY_TYPE_NORMAL) {
return state;
}
// Get interface address.
struct sockaddr_storage iface;
socklen_t iface_len = sizeof(iface);
if (getsockname(qdata->params->socket, (struct sockaddr *)&iface, &iface_len) != 0) {
knotd_mod_log(mod, LOG_ERR, "failed to get interface address");
return KNOTD_STATE_FAIL;
}
if (ctx->allow_addr.count > 0) {
if (!knotd_conf_addr_range_match(&ctx->allow_addr, qdata->params->remote)) {
qdata->rcode = KNOT_RCODE_NOTAUTH;
return KNOTD_STATE_FAIL;
}
}
if (ctx->allow_iface.count > 0) {
if (!knotd_conf_addr_range_match(&ctx->allow_iface, &iface)) {
qdata->rcode = KNOT_RCODE_NOTAUTH;
return KNOTD_STATE_FAIL;
}
}
return state;
}
int queryacl_load(knotd_mod_t *mod)
{
// Create module context.
queryacl_ctx_t *ctx = calloc(1, sizeof(queryacl_ctx_t));
if (ctx == NULL) {
return KNOT_ENOMEM;
}
ctx->allow_addr = knotd_conf_mod(mod, MOD_ADDRESS);
ctx->allow_iface = knotd_conf_mod(mod, MOD_INTERFACE);
knotd_mod_ctx_set(mod, ctx);
return knotd_mod_hook(mod, KNOTD_STAGE_BEGIN, queryacl_process);
}
void queryacl_unload(knotd_mod_t *mod)
{
queryacl_ctx_t *ctx = knotd_mod_ctx(mod);
if (ctx != NULL) {
knotd_conf_free(&ctx->allow_addr);
knotd_conf_free(&ctx->allow_iface);
}
free(ctx);
}
KNOTD_MOD_API(queryacl, KNOTD_MOD_FLAG_SCOPE_ANY,
queryacl_load, queryacl_unload, queryacl_conf, NULL);
.. _mod-queryacl:
``queryacl`` — Limit queries by remote address or target interface
==================================================================
This module provides a simple way to whitelist incoming queries
according to the query's source address or target interface.
It can be used e.g. to create a restricted-access subzone with delegations from the corresponding public zone.
The module may be enabled both globally and per-zone.
.. NOTE::
The module limits only regular queries. Notify, transfer and update are handled by :ref:`ACL<ACL>`.
Example
-------
::
mod-queryacl:
- id: default
address: [192.0.2.73-192.0.2.90, 203.0.113.0/24]
interface: 198.51.100
zone:
- domain: example.com
module: mod-queryacl/default
Module reference
----------------
::
mod-queryacl:
- id: STR
address: ADDR[/INT] | ADDR-ADDR ...
interface: ADDR[/INT] | ADDR-ADDR ...
.. _mod-queryacl_id:
id
..
A module identifier.
.. _mod-queryacl_address:
address
.......
A list of allowed ranges and/or subnets for query's source address. If the query's address does not fall into any
of the configured ranges, NOTAUTH rcode is returned.
.. _mod-queryacl_interface:
interface
.........
A list of allowed ranges and/or subnets for query's target interface. If the interface does not fall into any
of the configured ranges, NOTAUTH rcode is returned. Note that every interface used has to be configured in :ref:`listen<server_listen>`.
#!/usr/bin/env python3
'''Test for the queryacl module'''
from dnstest.utils import *
from dnstest.test import Test
from dnstest.libknot import libknot
from dnstest.module import ModQueryacl
import random
t = Test(address=4)
knot = t.server("knot")
zones = t.zone_rnd(3)
t.link(zones, knot)
knot.add_module(zones[0], ModQueryacl(address=["127.0.0.1/32", "127.0.0.2/32"]))
knot.add_module(zones[1], ModQueryacl(interface=["127.0.0.1/32", "127.0.0.2/32"]))
knot.add_module(zones[2], ModQueryacl(address=["127.0.0.1/32", "127.0.0.2/32"],
interface=["127.0.0.1/32", "127.0.0.2/32"]))
ctl = libknot.control.KnotCtl()
t.start()
knot.zones_wait(zones)
ctl.connect(os.path.join(knot.dir, "knot.sock"))
ctl.send_block(cmd="conf-begin")
ctl.receive_block()
port = str(knot.port)
ctl.send_block(cmd="conf-set", section="server", item="listen", data="127.0.0.1@"+port)
ctl.receive_block()
ctl.send_block(cmd="conf-set", section="server", item="listen", data="127.0.0.2@"+port)
ctl.receive_block()
ctl.send_block(cmd="conf-set", section="server", item="listen", data="127.0.0.3@"+port)
ctl.receive_block()
ctl.send_block(cmd="conf-commit")
ctl.receive_block()
# Test just address ACL.
resp = knot.dig(zones[0].name, "SOA", addr="127.0.0.3", source="127.0.0.3")
resp.check(rcode="NOTAUTH")
resp = knot.dig(zones[0].name, "SOA", addr="127.0.0.3", source="127.0.0.2")
resp.check(rcode="NOERROR")
resp = knot.dig(zones[0].name, "SOA", addr="127.0.0.2", source="127.0.0.3")
resp.check(rcode="NOTAUTH")
resp = knot.dig(zones[0].name, "SOA", addr="127.0.0.2", source="127.0.0.2")
resp.check(rcode="NOERROR")
# Test just interface ACL.
resp = knot.dig(zones[1].name, "SOA", addr="127.0.0.3", source="127.0.0.3")
resp.check(rcode="NOTAUTH")
resp = knot.dig(zones[1].name, "SOA", addr="127.0.0.3", source="127.0.0.2")
resp.check(rcode="NOTAUTH")
resp = knot.dig(zones[1].name, "SOA", addr="127.0.0.2", source="127.0.0.3")
resp.check(rcode="NOERROR")
resp = knot.dig(zones[1].name, "SOA", addr="127.0.0.2", source="127.0.0.2")
resp.check(rcode="NOERROR")
# Test both address and interface ACL.
resp = knot.dig(zones[2].name, "SOA", addr="127.0.0.3", source="127.0.0.3")
resp.check(rcode="NOTAUTH")
resp = knot.dig(zones[2].name, "SOA", addr="127.0.0.3", source="127.0.0.2")
resp.check(rcode="NOTAUTH")
resp = knot.dig(zones[2].name, "SOA", addr="127.0.0.2", source="127.0.0.3")
resp.check(rcode="NOTAUTH")
resp = knot.dig(zones[2].name, "SOA", addr="127.0.0.2", source="127.0.0.2")
resp.check(rcode="NOERROR")
t.end()
......@@ -23,6 +23,11 @@ class KnotConf(object):
def item_str(self, name, value):
self.conf += " %s: \"%s\"\n" % (name, value)
def item_list(self, name, values):
self.conf += " %s: [" % name
self.conf += ', '.join(str(value) for value in values)
self.conf += "]\n"
def id_item(self, name, value):
if not self.first_item:
self.conf += "\n"
......
......@@ -289,3 +289,33 @@ class ModCookies(KnotModule):
conf.end()
return conf
class ModQueryacl(KnotModule):
'''Query ACL module'''
mod_name = "queryacl"
def __init__(self, address=None, interface=None):
super().__init__()
self.address = address
self.interface = interface
def get_conf(self, conf=None):
if not conf:
conf = dnstest.config.KnotConf()
conf.begin(self.conf_name)
conf.id_item("id", self.conf_id)
if self.address:
if isinstance(self.address, list):
conf.item_list("address", self.address)
else:
conf.item("address", self.address)
if self.interface:
if isinstance(self.interface, list):
conf.item_list("interface", self.interface)
else:
conf.item("interface", self.interface)
conf.end()
return conf
......@@ -391,9 +391,9 @@ class Server(object):
f.write(self.get_config())
f.close()
def dig(self, rname, rtype, rclass="IN", udp=None, serial=None,
timeout=None, tries=3, flags="", bufsize=None, edns=None,
nsid=False, dnssec=False, log_no_sep=False, tsig=None):
def dig(self, rname, rtype, rclass="IN", udp=None, serial=None, timeout=None,
tries=3, flags="", bufsize=None, edns=None, nsid=False, dnssec=False,
log_no_sep=False, tsig=None, addr=None, source=None):
# Convert one item zone list to zone name.
if isinstance(rname, list):
......@@ -499,8 +499,15 @@ class Server(object):
if param != "self":
args[param] = params.locals[param]
if addr is None:
addr = self.addr
# Add source to dig flags if present
if source is not None:
dig_flags += " -b " + source
check_log("DIG %s %s %s @%s -p %i %s" %
(rname, rtype_str, rclass, self.addr, self.port, dig_flags))
(rname, rtype_str, rclass, addr, self.port, dig_flags))
# Set TSIG for a normal query if explicitly specified.
key_params = dict()
......@@ -527,20 +534,20 @@ class Server(object):
for t in range(tries):
try:
if rtype.upper() == "AXFR":
resp = dns.query.xfr(self.addr, rname, rtype, rclass,
resp = dns.query.xfr(addr, rname, rtype, rclass,
port=self.port, lifetime=timeout,
use_udp=udp, **key_params)
elif rtype.upper() == "IXFR":
resp = dns.query.xfr(self.addr, rname, rtype, rclass,
resp = dns.query.xfr(addr, rname, rtype, rclass,
port=self.port, lifetime=timeout,
use_udp=udp, serial=int(serial),
**key_params)
elif udp:
resp = dns.query.udp(query, self.addr, port=self.port,
timeout=timeout)
resp = dns.query.udp(query, addr, port=self.port,
timeout=timeout, source=source)
else:
resp = dns.query.tcp(query, self.addr, port=self.port,
timeout=timeout)
resp = dns.query.tcp(query, addr, port=self.port,
timeout=timeout, source=source)
if not log_no_sep:
detail_log(SEP)
......
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