Commit 67689fad authored by Daniel Salzman's avatar Daniel Salzman

Merge branch 'new_geo_module' into 'master'

Module for response tailoring according to client's subnet or geographic location

Closes #563 and #413

See merge request !910
parents e652afeb f4e6b90a
......@@ -144,6 +144,9 @@ src/knot/journal/serialization.h
src/knot/modules/cookies/cookies.c
src/knot/modules/dnsproxy/dnsproxy.c
src/knot/modules/dnstap/dnstap.c
src/knot/modules/geoip/geodb.c
src/knot/modules/geoip/geodb.h
src/knot/modules/geoip/geoip.c
src/knot/modules/noudp/noudp.c
src/knot/modules/onlinesign/nsec_next.c
src/knot/modules/onlinesign/nsec_next.h
......
......@@ -306,6 +306,7 @@ doc_modules=""
KNOT_MODULE([cookies], "yes")
KNOT_MODULE([dnsproxy], "yes", "non-shareable")
KNOT_MODULE([dnstap], "no")
KNOT_MODULE([geoip], "yes")
KNOT_MODULE([noudp], "yes")
KNOT_MODULE([onlinesign], "yes", "non-shareable")
KNOT_MODULE([rrl], "yes")
......@@ -344,6 +345,41 @@ AM_CONDITIONAL([HAVE_DNSTAP], test "$enable_dnstap" != "no")
AM_CONDITIONAL([HAVE_LIBDNSTAP], test "$enable_dnstap" != "no" -o \
"$STATIC_MODULE_dnstap" != "no" -o \
"$SHARED_MODULE_dnstap" != "no")
# MaxMind DB for the GeoIP module
AC_ARG_ENABLE([maxminddb],
AS_HELP_STRING([--enable-maxminddb=auto|yes|no], [enable MaxMind DB [default=auto]]),
[enable_maxminddb="$enableval"], [enable_maxminddb=auto])
AS_IF([test "$enable_daemon" = "no"],[enable_maxminddb=no])
AS_CASE([$enable_maxminddb],
[no],[],
[auto],[PKG_CHECK_MODULES([libmaxminddb], [libmaxminddb], [enable_maxminddb=yes], [enable_maxminddb=no])],
[yes], [PKG_CHECK_MODULES([libmaxminddb], [libmaxminddb])],
[*],[
save_CFLAGS="$CFLAGS"
save_LIBS="$LIBS"
AS_IF([test "$enable_maxminddb" != ""],[
LIBS="$LIBS -L$enable_maxminddb"
CFLAGS="$CFLAGS -I$enable_maxminddb/include"
])
AC_SEARCH_LIBS([MMDB_open], [maxminddb], [
AS_IF([test "$enable_maxminddb" != ""], [
libmaxminddb_CFLAGS="-I$enable_maxminddb/include"
libmaxminddb_LIBS="-L$enable_maxminddb -lmaxminddb"
],[
libmaxminddb_CFLAGS=""
libmaxminddb_LIBS="$ac_cv_search_MMDB_open"
])
],[AC_MSG_ERROR("not found in `$enable_maxminddb'")])
CFLAGS="$save_CFLAGS"
LIBS="$save_LIBS"
AC_SUBST([libmaxminddb_CFLAGS])
AC_SUBST([libmaxminddb_LIBS])
enable_maxminddb=yes
])
AS_IF([test "$enable_maxminddb" = yes], [AC_DEFINE([HAVE_MAXMINDDB], [1], [Define to 1 to enable MaxMind DB.])])
AM_CONDITIONAL([HAVE_MAXMINDDB], [test "$enable_maxminddb" = yes])
dnl Check for LMDB
lmdb_MIN_VERSION_MAJOR=0
......@@ -582,6 +618,7 @@ result_msg_base=" Knot DNS $VERSION
Fast zone parser: ${enable_fastparser}
Utilities with IDN: ${with_libidn}
Utilities with Dnstap: ${enable_dnstap}
MaxMind DB support: ${enable_maxminddb}
Systemd integration: ${enable_systemd}
PKCS #11 support: ${enable_pkcs11}
Ed25519 support: ${enable_ed25519}
......
......@@ -56,6 +56,7 @@ Remarkable module extensions:
* DNS request traffic statistics
* Dnstap traffic logging
* Online DNSSEC signing
* GeoIP response tailoring supporting ECS and DNSSEC
License
=======
......
......@@ -188,6 +188,7 @@ pkglib_LTLIBRARIES =
include $(srcdir)/knot/modules/cookies/Makefile.inc
include $(srcdir)/knot/modules/dnsproxy/Makefile.inc
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/rrl/Makefile.inc
......
......@@ -131,44 +131,6 @@ static bool valid_signature_exists(const knot_rrset_t *covered,
return false;
}
/*!
* \brief Check if key can be used to sign given RR.
*
* \param key Zone key.
* \param covered RR to be checked.
*
* \return The RR should be signed.
*/
static bool use_key(const zone_key_t *key, const knot_rrset_t *covered)
{
assert(key);
assert(covered);
if (!key->is_active) {
return false;
}
// this may be a problem with offline KSK
bool cds_sign_by_ksk = true;
assert(key->is_zsk || key->is_ksk);
bool is_apex = knot_dname_is_equal(covered->owner,
dnssec_key_get_dname(key->key));
if (!is_apex) {
return key->is_zsk;
}
switch (covered->type) {
case KNOT_RRTYPE_DNSKEY:
return key->is_ksk;
case KNOT_RRTYPE_CDS:
case KNOT_RRTYPE_CDNSKEY:
return (cds_sign_by_ksk ? key->is_ksk : key->is_zsk);
default:
return key->is_zsk;
}
}
/*!
* \brief Check if valid signature exist for all keys for a given RR set.
*
......@@ -189,7 +151,7 @@ static bool all_signatures_exist(const knot_rrset_t *covered,
for (int i = 0; i < zone_keys->count; i++) {
zone_key_t *key = &zone_keys->keys[i];
if (!use_key(key, covered)) {
if (!knot_zone_sign_use_key(key, covered)) {
continue;
}
......@@ -337,7 +299,7 @@ static int add_missing_rrsigs(const knot_rrset_t *covered,
for (int i = 0; i < zone_keys->count; i++) {
const zone_key_t *key = &zone_keys->keys[i];
if (!use_key(key, covered)) {
if (!knot_zone_sign_use_key(key, covered)) {
continue;
}
......@@ -1005,6 +967,37 @@ cleanup:
return ret;
}
bool knot_zone_sign_use_key(const zone_key_t *key, const knot_rrset_t *covered)
{
if (key == NULL || covered == NULL) {
return KNOT_EINVAL;
}
if (!key->is_active) {
return false;
}
// this may be a problem with offline KSK
bool cds_sign_by_ksk = true;
assert(key->is_zsk || key->is_ksk);
bool is_apex = knot_dname_is_equal(covered->owner,
dnssec_key_get_dname(key->key));
if (!is_apex) {
return key->is_zsk;
}
switch (covered->type) {
case KNOT_RRTYPE_DNSKEY:
return key->is_ksk;
case KNOT_RRTYPE_CDS:
case KNOT_RRTYPE_CDNSKEY:
return (cds_sign_by_ksk ? key->is_ksk : key->is_zsk);
default:
return key->is_zsk;
}
}
bool knot_zone_sign_soa_expired(const zone_contents_t *zone,
const zone_keyset_t *zone_keys,
const kdnssec_ctx_t *dnssec_ctx)
......
......@@ -35,6 +35,16 @@ int knot_zone_sign_update_dnskeys(zone_update_t *update,
zone_keyset_t *zone_keys,
const kdnssec_ctx_t *dnssec_ctx);
/*!
* \brief Check if key can be used to sign given RR.
*
* \param key Zone key.
* \param covered RR to be checked.
*
* \return The RR should be signed.
*/
bool knot_zone_sign_use_key(const zone_key_t *key, const knot_rrset_t *covered);
/*!
* \brief Update zone signatures and store performed changes in update.
*
......
......@@ -439,6 +439,7 @@ typedef enum {
/*! Query module processing stages. */
typedef enum {
KNOTD_STAGE_BEGIN = 0, /*!< Before query processing. */
KNOTD_STAGE_PREANSWER, /*!< Before section processing. */
KNOTD_STAGE_ANSWER, /*!< Answer section processing. */
KNOTD_STAGE_AUTHORITY, /*!< Authority section processing. */
KNOTD_STAGE_ADDITIONAL, /*!< Additional section processing. */
......@@ -493,4 +494,38 @@ int knotd_mod_hook(knotd_mod_t *mod, knotd_stage_t stage, knotd_mod_hook_f hook)
*/
int knotd_mod_in_hook(knotd_mod_t *mod, knotd_stage_t stage, knotd_mod_in_hook_f hook);
/*** DNSSEC API. ***/
/*!
* Initializes DNSSEC signing context.
*
* \param[in] mod Module context.
*
* \return Error code, KNOT_EOK if success.
*/
int knotd_mod_dnssec_init(knotd_mod_t *mod);
/*!
* Loads available DNSSEC signing keys.
*
* \param[in] mod Module context.
* \param[in] verbose Print key summary into log indication.
*
* \return Error code, KNOT_EOK if success.
*/
int knotd_mod_dnssec_load_keyset(knotd_mod_t *mod, bool verbose);
/*!
* Generates RRSIGs for given RRSet.
*
* \param[in] mod Module context.
* \param[out] rrsigs Output RRSIG RRSet.
* \param[in] rrset Input RRSet to generate RRSIGs for.
* \param[in] mm Memory context.
*
* \return Error code, KNOT_EOK if success.
*/
int knotd_mod_dnssec_sign_rrset(knotd_mod_t *mod, knot_rrset_t *rrsigs,
const knot_rrset_t *rrset, knot_mm_t *mm);
/*! @} */
knot_modules_geoip_la_SOURCES = knot/modules/geoip/geoip.c \
knot/modules/geoip/geodb.c \
knot/modules/geoip/geodb.h
EXTRA_DIST += knot/modules/geoip/geoip.rst
if STATIC_MODULE_geoip
libknotd_la_SOURCES += $(knot_modules_geoip_la_SOURCES)
libknotd_la_CPPFLAGS += $(libmaxminddb_CFLAGS)
libknotd_la_LIBADD += $(libmaxminddb_LIBS)
endif
if SHARED_MODULE_geoip
knot_modules_geoip_la_LDFLAGS = $(KNOTD_MOD_LDFLAGS)
knot_modules_geoip_la_CPPFLAGS = $(KNOTD_MOD_CPPFLAGS)
pkglib_LTLIBRARIES += knot/modules/geoip.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 <http://www.gnu.org/licenses/>.
*/
#include "knot/modules/geoip/geodb.h"
#include "contrib/strtonum.h"
#include "contrib/string.h"
#if HAVE_MAXMINDDB
static const uint16_t type_map[] = {
[GEODB_KEY_ID] = MMDB_DATA_TYPE_UINT32,
[GEODB_KEY_TXT] = MMDB_DATA_TYPE_UTF8_STRING
};
#endif
int parse_geodb_path(geodb_path_t *path, const char *input)
{
if (path == NULL || input == NULL) {
return -1;
}
// Parse optional type of key.
path->type = GEODB_KEY_TXT;
const char *delim = input;
if (input[0] == '(') {
delim = strchrnul(input, ')');
if (*delim != ')') {
return -1;
}
input++;
char *type = sprintf_alloc("%.*s", delim - input, input);
const knot_lookup_t *table = knot_lookup_by_name(geodb_key_types, type);
free(type);
if (table == NULL) {
return -1;
}
path->type = table->id;
input = delim + 1;
}
// Parse the path.
uint16_t len = 0;
while (1) {
delim = strchrnul(input, '/');
path->path[len] = malloc(delim - input + 1);
if (path->path[len] == NULL) {
return -1;
}
memcpy(path->path[len], input, delim - input);
path->path[len][delim - input] = '\0';
len++;
if (*delim == 0 || len == GEODB_MAX_PATH_LEN) {
break;
}
input = delim + 1;
}
return (len == 0) ? -1 : 0;
}
int parse_geodb_data(const char *input, void **geodata, uint32_t *geodata_len,
uint8_t *geodepth, geodb_path_t *path, uint16_t path_cnt)
{
for (uint16_t i = 0; i < path_cnt; i++) {
const char *delim = strchrnul(input, ';');
uint16_t key_len = delim - input;
if (key_len > 0 && !(key_len == 1 && *input == '*')) {
*geodepth = i + 1;
switch (path[i].type) {
case GEODB_KEY_TXT:
geodata[i] = malloc(key_len + 1);
if (geodata[i] == NULL) {
return -1;
}
memcpy(geodata[i], input, key_len);
((char *)geodata[i])[key_len] = '\0';
geodata_len[i] = key_len;
break;
case GEODB_KEY_ID:
geodata[i] = malloc(sizeof(uint32_t));
if (geodata[i] == NULL) {
return -1;
}
if (str_to_u32(input, (uint32_t *)geodata[i]) != KNOT_EOK) {
return -1;
}
geodata_len[i] = sizeof(uint32_t);
break;
default:
assert(0);
return -1;
}
}
if (*delim == '\0') {
break;
}
input = delim + 1;
}
return 0;
}
bool geodb_available(void)
{
#if HAVE_MAXMINDDB
return true;
#else
return false;
#endif
}
geodb_t *geodb_open(const char *filename)
{
#if HAVE_MAXMINDDB
MMDB_s *db = calloc(1, sizeof(MMDB_s));
if (db == NULL) {
return NULL;
}
int mmdb_error = MMDB_open(filename, MMDB_MODE_MMAP, db);
if (mmdb_error != MMDB_SUCCESS) {
return NULL;
}
return db;
#else
return NULL;
#endif
}
geodb_data_t *geodb_alloc_entries(uint16_t count)
{
#if HAVE_MAXMINDDB
MMDB_entry_data_s *entries = calloc(count, sizeof(MMDB_entry_data_s));
return entries;
#else
return NULL;
#endif
}
void geodb_close(geodb_t *geodb)
{
#if HAVE_MAXMINDDB
MMDB_close(geodb);
#endif
}
int geodb_query(geodb_t *geodb, geodb_data_t *entries, struct sockaddr *remote,
geodb_path_t *paths, uint16_t path_cnt, uint16_t *netmask)
{
#if HAVE_MAXMINDDB
int mmdb_error = 0;
MMDB_lookup_result_s res;
res = MMDB_lookup_sockaddr(geodb, remote, &mmdb_error);
if (mmdb_error != MMDB_SUCCESS || !res.found_entry) {
return -1;
}
// Save netmask.
*netmask = res.netmask;
for (uint16_t i = 0; i < path_cnt; i++) {
// Get the value of the next key.
mmdb_error = MMDB_aget_value(&res.entry, &entries[i], (const char *const*)paths[i].path);
if (mmdb_error != MMDB_SUCCESS && mmdb_error != MMDB_LOOKUP_PATH_DOES_NOT_MATCH_DATA_ERROR) {
return -1;
}
if (mmdb_error == MMDB_LOOKUP_PATH_DOES_NOT_MATCH_DATA_ERROR || !entries[i].has_data) {
entries[i].has_data = false;
continue;
}
// Check the type.
if (entries[i].type != type_map[paths[i].type]) {
entries[i].has_data = false;
continue;
}
}
return 0;
#else
return -1;
#endif
}
bool remote_in_geo(void **geodata, uint32_t *geodata_len, uint16_t geodepth, geodb_data_t *entries)
{
#if HAVE_MAXMINDDB
for (int i = 0; i < geodepth; i++) {
// Nothing to do if current geodata do not specify this key.
if (geodata[i] == NULL) {
continue;
}
if (!entries[i].has_data) {
return false;
}
switch (entries[i].type) {
case MMDB_DATA_TYPE_UTF8_STRING:
if (geodata_len[i] != entries[i].data_size ||
memcmp(geodata[i], entries[i].utf8_string, geodata_len[i]) != 0) {
return false;
}
break;
case MMDB_DATA_TYPE_UINT32:
if (*((uint32_t *)geodata[i]) != entries[i].uint32) {
return false;
}
break;
default:
return false;
}
}
return true;
#else
return false;
#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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <libknot/libknot.h>
#if HAVE_MAXMINDDB
#include <maxminddb.h>
#endif
#if HAVE_MAXMINDDB
#define geodb_t MMDB_s
#define geodb_data_t MMDB_entry_data_s
#else
#define geodb_t void
#define geodb_data_t void
#endif
// MaxMind DB related constants.
#define GEODB_MAX_PATH_LEN 8
#define GEODB_MAX_DEPTH 8
typedef enum {
GEODB_KEY_ID,
GEODB_KEY_TXT
} geodb_key_type_t;
static const knot_lookup_t geodb_key_types[] = {
{ GEODB_KEY_ID, "id" },
{ GEODB_KEY_TXT, "" }
};
typedef struct {
geodb_key_type_t type;
char *path[GEODB_MAX_PATH_LEN + 1]; // MMDB_aget_value() requires last member to be NULL.
} geodb_path_t;
int parse_geodb_path(geodb_path_t *path, const char *input);
int parse_geodb_data(const char *input, void **geodata, uint32_t *geodata_len,
uint8_t *geodepth, geodb_path_t *path, uint16_t path_cnt);
bool geodb_available(void);
geodb_t *geodb_open(const char *filename);
geodb_data_t *geodb_alloc_entries(uint16_t count);
void geodb_close(geodb_t *geodb);
int geodb_query(geodb_t *geodb, geodb_data_t *entries, struct sockaddr *remote,
geodb_path_t *paths, uint16_t path_cnt, uint16_t *netmask);
bool remote_in_geo(void **geodata, uint32_t *geodata_len, uint16_t geodepth, geodb_data_t *entries);
This diff is collapsed.
.. _mod-geoip:
``geoip`` — Geography-based responses
=====================================
This module offers response tailoring based on client's
subnet or geographic location. It supports GeoIP databases
in the MaxMind DB format, such as `GeoIP2 <https://dev.maxmind.com/geoip/geoip2/downloadable/>`_
or the free version `GeoLite2 <https://dev.maxmind.com/geoip/geoip2/geolite2/>`_.
The module can be enabled only per zone.
.. NOTE::
If :ref:`EDNS Client Subnet<server_edns-client-subnet>` support is enabled
and if a query contains this option, the module takes advantage of this
information to provide a more accurate response.
DNSSEC support
--------------
There are two ways to enable DNSSEC signing of tailored responses.
If automatic DNSSEC signing is enabled, record signatures are precomputed when the module is loaded.
This has a speed benefit, however note that every RRset configured in the module should
have a **default** RRset of the same type contained in the zone, so that the NSEC(3)
chain can be built correctly. Also, it is STRONGLY RECOMMENDED to use manual key rollover in this setting,
as the module has to be reloaded when the signing key changes.
Alternatively, the :ref:`geoip<mod-geoip>` module may be combined with the :ref:`onlinesign<mod-onlinesign>` module
and the tailored responses can be signed on the fly. This approach is more computationally demanding for the server.
Example
-------
* An example configuration.::
mod-geoip:
- id: default
config-file: /path/to/geo.conf
ttl: 20
mode: geodb
geodb-file: /path/to/GeoLite2-City.mmdb
geodb-key: [ country/iso_code, city/names/en ]
zone:
- domain: example.com.
module: mod-geoip/default
Configuration file
------------------
Every instance of the module requires an additional :ref:`mod-geoip_config-file` in which the desired responses to queries from
various locations are configured. This file has the following simple format:
::
domain-name1:
- geo|net: location1
RR-Type1: RDATA
RR-Type2: RDATA
...
- geo|net: location2
RR-Type1: RDATA
...
domain-name2:
...
Example
-------
* Example :ref:`mod-geoip_config-file` for subnets
::
foo.example.com:
- net: 10.0.0.0/24
A: [ 192.168.1.1, 192.168.1.2 ]
AAAA: [ 2001:DB8::1, 2001:DB8::2 ]
TXT: "subnet 10.0.0.0/24"
...
bar.example.com:
- net: 2001:DB8::/32
A: 192.168.1.3
AAAA: 2001:DB8::3
TXT: "subnet 2001:DB8::/32"
...
* Example :ref:`mod-geoip_config-file` for geographic locations
::
foo.example.com:
- geo: "CZ;Prague"
CNAME: cz.foo.example.com
- geo: "US;Las Vegas"
CNAME: vegas.foo.example.net
- geo: "US;*"
CNAME: us.foo.example.net
...
Module reference
----------------
::
mod-geoip:
- id: STR
config-file: STR
ttl: TIME
mode: geodb | subnet
geodb-file: STR
geodb-key: STR ...
.. _mod-geoip_id:
id
..
A module identifier.
.. _mod-geoip_config-file:
config-file
...........
Full path to the response configuration file as described above.
*Required*
.. _mod-geoip_ttl:
ttl
...
The time to live of Resource Records returned by the module.
*Default:* 60
.. _mod-geoip_mode:
mode
....
The mode of operation of the module. When set to **subnet**, responses
are tailored according to subnets. When set to **geodb**, responses
are tailored according to geographic data retrieved from the configured
database.
.. _mod-geoip_geodb-file:
geodb-file
..........
Full path to a .mmdb file containing the GeoIP database.
*Reqired if* **mode** *is set to* **geodb**
.. _mod-geoip_geodb-key:
geodb-key
.........
Multi-valued item, can be specified up to **8** times. Each **geodb-key** specifies
a path to a key in a node in the supplied GeoIP database. The module currently supports
two types of values: **string** or **32-bit unsigned int**. In the latter
case, the key has to be prefixed with **(id)**. Common choices of keys include:
* **continent/code**
* **country/iso_code**
* **(id)country/geoname_id**
* **city/names/en**
* **(id)city/geoname_id**
* **isp**
* ...
In the zone's config file for the module the values of the keys are entered in the same order
as the keys in the module's configuration, separated by a semicolon. Enter the value **"*"**
if the key is allowed to have any value.