Commit b70f33f1 authored by Daniel Salzman's avatar Daniel Salzman

Merge branch 'mod-queryacl' into 'master'

Query ACL module

See merge request !932
parents 1c4c5908 fc628627
......@@ -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
......
......@@ -336,6 +336,17 @@ knotd_conf_t knotd_conf_zone(knotd_mod_t *mod, const yp_name_t *item_name,
knotd_conf_t knotd_conf_check_item(knotd_conf_check_args_t *args,
const yp_name_t *item_name);
/*!
* \brief Checks if address is in at least one of given ranges.
*
* \param[in] range
* \param[in] addr
*
* \return true if addr is in at least one range, false otherwise.
*/
bool knotd_conf_addr_range_match(const knotd_conf_t *range,
const struct sockaddr_storage *addr);
/*!
* Deallocates multi-valued configuration values.
*
......
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>`.
......@@ -14,7 +14,6 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "contrib/sockaddr.h"
#include "knot/include/module.h"
#include "knot/nameserver/process_query.h" // Dependency on qdata->extra!
#include "knot/modules/rrl/functions.h"
......@@ -75,30 +74,6 @@ static const knot_dname_t *name_from_authrr(const knot_rrset_t *rr)
return rr->owner;
}
static bool addr_range_match(const knotd_conf_t *range, const struct sockaddr_storage *addr)
{
assert(range && addr);
for (size_t i = 0; i < range->count; i++) {
knotd_conf_val_t *val = &range->multi[i];
if (val->addr_max.ss_family == AF_UNSPEC) {
if (sockaddr_net_match((struct sockaddr *)addr,
(struct sockaddr *)&val->addr,
val->addr_mask)) {
return true;
}
} else {
if (sockaddr_range_match((struct sockaddr *)addr,
(struct sockaddr *)&val->addr,
(struct sockaddr *)&val->addr_max)) {
return true;
}
}
}
return false;
}
static knotd_state_t ratelimit_apply(knotd_state_t state, knot_pkt_t *pkt,
knotd_qdata_t *qdata, knotd_mod_t *mod)
{
......@@ -117,7 +92,7 @@ static knotd_state_t ratelimit_apply(knotd_state_t state, knot_pkt_t *pkt,
}
// Exempt clients.
if (addr_range_match(&ctx->whitelist, qdata->params->remote)) {
if (knotd_conf_addr_range_match(&ctx->whitelist, qdata->params->remote)) {
return state;
}
......
......@@ -20,6 +20,7 @@
#include <stdlib.h>
#include <string.h>
#include "contrib/sockaddr.h"
#include "libknot/attribute.h"
#include "knot/common/log.h"
#include "knot/conf/module.h"
......@@ -498,6 +499,34 @@ knotd_conf_t knotd_conf_check_item(knotd_conf_check_args_t *args,
return out;
}
_public_
bool knotd_conf_addr_range_match(const knotd_conf_t *range,
const struct sockaddr_storage *addr)
{
if (range == NULL || addr == NULL) {
return false;
}
for (size_t i = 0; i < range->count; i++) {
knotd_conf_val_t *val = &range->multi[i];
if (val->addr_max.ss_family == AF_UNSPEC) {
if (sockaddr_net_match((struct sockaddr *)addr,
(struct sockaddr *)&val->addr,
val->addr_mask)) {
return true;
}
} else {
if (sockaddr_range_match((struct sockaddr *)addr,
(struct sockaddr *)&val->addr,
(struct sockaddr *)&val->addr_max)) {
return true;
}
}
}
return false;
}
_public_
void knotd_conf_free(knotd_conf_t *conf)
{
......
#!/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