Commit 17d8c5de authored by Mark Karpilovskij's avatar Mark Karpilovskij

geoip: initial commit

parent 4d34c15e
......@@ -144,6 +144,7 @@ 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/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}
......
......@@ -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
......
knot_modules_geoip_la_SOURCES = knot/modules/geoip/geoip.c
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/include/module.h"
#include "libknot/libknot.h"
#include "contrib/qp-trie/trie.h"
#include "contrib/mempattern.h"
#include "knot/conf/conf.h"
#include "contrib/ucw/lists.h"
#include "contrib/sockaddr.h"
#include "contrib/openbsd/strlcpy.h"
#include "contrib/string.h"
#include "libzscanner/scanner.h"
// Next dependecies force static module!
#include "knot/dnssec/rrset-sign.h"
#include "knot/dnssec/zone-keys.h"
#include "knot/nameserver/query_module.h"
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
<<<<<<< HEAD
#if HAVE_MAXMINDDB
#include <maxminddb.h>
#endif
#define MOD_GEO_CONF_FILE "\x0D""geo-conf-file"
#define MOD_TTL "\x03""ttl"
#define MOD_MODE "\x04""mode"
#define MOD_GEODB_FILE "\x0A""geodb-file"
// MaxMind DB related constants.
#define ISO_CODE_LEN 2
=======
#define MOD_CONFIG_FILE "\x0B""config-file"
#define MOD_TTL "\x03""ttl"
#define MOD_MODE "\x04""mode"
#define MOD_GEODB_FILE "\x0A""geodb-file"
#define MOD_GEODB_KEY "\x09""geodb-key"
>>>>>>> fb31d19a2... blbost
enum operation_mode {
MODE_SUBNET,
MODE_GEODB
};
static const knot_lookup_t modes[] = {
{ MODE_SUBNET, "subnet" },
{ MODE_GEODB, "geodb" },
{ 0, NULL }
};
const yp_item_t geoip_conf[] = {
{ MOD_CONFIG_FILE, YP_TSTR, YP_VNONE },
{ MOD_TTL, YP_TINT, YP_VINT = { 0, UINT32_MAX, 60, YP_STIME } },
{ MOD_MODE, YP_TOPT, YP_VOPT = { modes, MODE_SUBNET} },
{ MOD_GEODB_FILE, YP_TSTR, YP_VNONE },
{ MOD_GEODB_KEY, YP_TSTR, YP_VSTR = { "country/iso_code" }, YP_FMULTI },
{ NULL }
};
int geoip_conf_check(knotd_conf_check_args_t *args)
{
knotd_conf_t conf = knotd_conf_check_item(args, MOD_CONFIG_FILE);
if (conf.count == 0) {
args->err_str = "no configuration file specified";
return KNOT_EINVAL;
}
conf = knotd_conf_check_item(args, MOD_MODE);
if (conf.count == 1 && conf.single.option == MODE_GEODB) {
conf = knotd_conf_check_item(args, MOD_GEODB_FILE);
if (conf.count == 0) {
args->err_str = "no geodb file specified while in geodb mode";
return KNOT_EINVAL;
}
}
return KNOT_EOK;
}
typedef struct {
enum operation_mode mode;
uint32_t ttl;
trie_t *geo_trie;
bool dnssec;
zone_keyset_t keyset;
kdnssec_ctx_t kctx;
#if HAVE_MAXMINDDB
MMDB_s db;
#endif
} geoip_ctx_t;
typedef struct {
uint8_t prefix;
in_addr_t addr;
} subnet_t;
typedef struct {
char country_iso[3];
char *city;
uint16_t city_len;
} geodata_t;
typedef struct {
subnet_t subnet;
geodata_t geodata;
size_t count, avail;
knot_rrset_t *rrsets;
knot_rrset_t *rrsigs;
} geo_view_t;
typedef struct {
size_t count, avail;
geo_view_t *views;
} geo_trie_val_t;
static int add_view_to_trie(knot_dname_t *owner, geo_view_t view, geoip_ctx_t *ctx)
{
int ret = KNOT_EOK;
// Find the node belonging to the owner.
trie_val_t *val = trie_get_ins(ctx->geo_trie, (char *)owner, knot_dname_size(owner));
geo_trie_val_t *cur_val = *val;
if (cur_val == NULL) {
// Create new node value.
geo_trie_val_t *new_val = calloc(1, sizeof(geo_trie_val_t));
new_val->avail = 1;
new_val->count = 1;
new_val->views = malloc(sizeof(geo_view_t));
new_val->views[0] = view;
// Add new value to trie.
*val = new_val;
} else {
// Double the views array in size if necessary.
if (cur_val->avail >= cur_val->count) {
void *alloc_ret = realloc(cur_val->views,
2 * cur_val->avail * sizeof(geo_view_t));
if (alloc_ret == NULL) {
return KNOT_ENOMEM;
}
cur_val->views = alloc_ret;
cur_val->avail *= 2;
}
// Insert new element.
cur_val->views[cur_val->count++] = view;
}
return ret;
}
static bool addr_in_subnet(in_addr_t addr, subnet_t subnet)
{
uint8_t mask_data[sizeof(in_addr_t)] = { 0 };
for (int i = 0; i < subnet.prefix / 8; i++) {
mask_data[i] = UINT8_MAX;
}
if (subnet.prefix % 8 != 0) {
mask_data[subnet.prefix / 8] = ((1 << (subnet.prefix % 8)) - 1) << (8 - subnet.prefix % 8);
}
in_addr_t *mask = (in_addr_t *)mask_data;
return (addr & *mask) == subnet.addr;
}
static bool addr_in_geo(knotd_mod_t *mod, const struct sockaddr *addr, geodata_t geodata, uint16_t *netmask)
{
#if HAVE_MAXMINDDB
geoip_ctx_t *ctx = (geoip_ctx_t *)knotd_mod_ctx(mod);
int mmdb_error = 0;
MMDB_lookup_result_s res;
res = MMDB_lookup_sockaddr(&ctx->db, addr, &mmdb_error);
if (mmdb_error != MMDB_SUCCESS) {
knotd_mod_log(mod, LOG_ERR, "a lookup error in MMDB occured");
return false;
}
if (!res.found_entry) {
return false;
}
// Set netmask value.
*netmask = res.netmask;
MMDB_entry_data_s iso_entry;
MMDB_entry_data_s city_entry;
// Get remote country ISO code.
mmdb_error = MMDB_get_value(&res.entry, &iso_entry, "country", "iso_code", NULL);
if (mmdb_error != MMDB_SUCCESS) {
knotd_mod_log(mod, LOG_ERR, "an error in MMDB occured (country)");
return false;
}
if (!iso_entry.has_data || iso_entry.type != MMDB_DATA_TYPE_UTF8_STRING
|| iso_entry.data_size != ISO_CODE_LEN) {
return false;
}
// Get remote city name.
mmdb_error = MMDB_get_value(&res.entry, &city_entry, "city", "names", "en", NULL);
if (mmdb_error != MMDB_SUCCESS &&
mmdb_error != MMDB_LOOKUP_PATH_DOES_NOT_MATCH_DATA_ERROR) {
knotd_mod_log(mod, LOG_ERR, "an error in MMDB occured (city)");
return false;
}
// Compare geodata.
if (memcmp(geodata.country_iso, iso_entry.utf8_string, ISO_CODE_LEN) != 0) {
return false;
}
if (geodata.city == NULL) {
return true;
}
if (!city_entry.has_data || city_entry.type != MMDB_DATA_TYPE_UTF8_STRING
|| geodata.city_len != city_entry.data_size
|| memcmp(geodata.city, city_entry.utf8_string, geodata.city_len)) {
return false;
}
return true;
#endif
return false;
}
static int finalize_geo_view(geo_view_t *view, knot_dname_t *owner, zone_key_t *key, geoip_ctx_t *ctx)
{
if (view == NULL) {
return KNOT_EOK;
}
int ret = KNOT_EOK;
if (key != NULL) {
view->rrsigs = malloc(sizeof(knot_rrset_t) * view->count);
if (view->rrsigs == NULL) {
return KNOT_ENOMEM;
}
for (size_t i = 0; i < view->count; i++) {
knot_dname_t *owner_cpy = knot_dname_copy(owner, NULL);
if (owner_cpy == NULL) {
return KNOT_ENOMEM;
}
knot_rrset_init(&view->rrsigs[i], owner_cpy, KNOT_RRTYPE_RRSIG,
KNOT_CLASS_IN, ctx->ttl);
ret = knot_sign_rrset(&view->rrsigs[i], &view->rrsets[i],
key->key, key->ctx, &ctx->kctx, NULL);
if (ret != KNOT_EOK) {
return ret;
}
}
}
ret = add_view_to_trie(owner, *view, ctx);
return ret;
}
static int init_geo_view(geo_view_t *view)
{
if (view == NULL) {
return KNOT_EINVAL;
}
view->count = 0;
view->avail = 1;
view->rrsigs = NULL;
view->rrsets = malloc(sizeof(knot_rrset_t));
if (view->rrsets == NULL) {
return KNOT_ENOMEM;
}
return KNOT_EOK;
}
static void clear_geo_view(geo_view_t *view)
{
if (view->geodata.city != NULL) {
free(view->geodata.city);
}
for (int j = 0; j < view->count; j++) {
knot_rrset_clear(&view->rrsets[j], NULL);
if (view->rrsigs != NULL) {
knot_rrset_clear(&view->rrsigs[j], NULL);
}
}
free(view->rrsets);
free(view->rrsigs);
}
static int geo_conf_yparse(knotd_mod_t *mod, geoip_ctx_t *ctx)
{
int ret = KNOT_EOK;
yp_parser_t *yp = NULL;
zs_scanner_t *scanner = NULL;
knot_dname_t owner_buff[KNOT_DNAME_MAXLEN];
knot_dname_t *owner = NULL;
geo_view_t *view = NULL;
yp = malloc(sizeof(yp_parser_t));
if (yp == NULL) {
return KNOT_ENOMEM;
}
yp_init(yp);
knotd_conf_t conf = knotd_conf_mod(mod, MOD_CONFIG_FILE);
ret = yp_set_input_file(yp, conf.single.string);
if (ret != KNOT_EOK) {
knotd_mod_log(mod, LOG_ERR, "failed to load configuration file");
goto cleanup;
}
scanner = malloc(sizeof(zs_scanner_t));
if (scanner == NULL) {
ret = KNOT_ENOMEM;
goto cleanup;
}
if (zs_init(scanner, NULL, KNOT_CLASS_IN, ctx->ttl) != 0) {
ret = KNOT_EPARSEFAIL;
goto cleanup;
}
zone_key_t *key = NULL;
if (ctx->dnssec) {
for (size_t i = 0; i < ctx->keyset.count; i++) {
if (ctx->keyset.keys[i].is_zsk) {
key = &ctx->keyset.keys[i];
}
}
}
while (1) {
ret = yp_parse(yp);
if (ret == KNOT_EOF) {
ret = finalize_geo_view(view, owner, key, ctx);
goto cleanup;
}
if (ret != KNOT_EOK) {
knotd_mod_log(mod, LOG_ERR, "failed to parse configuration file (%s)", knot_strerror(ret));
goto cleanup;
}
if (yp->event != YP_EKEY1) {
ret = finalize_geo_view(view, owner, key, ctx);
if (ret != KNOT_EOK) {
goto cleanup;
}
}
if (yp->event == YP_EKEY0) {
owner = knot_dname_from_str(owner_buff, yp->key, sizeof(owner_buff));
if (owner == NULL) {
ret = KNOT_EINVAL;
knotd_mod_log(mod, LOG_ERR, "invalid domain name in config");
goto cleanup;
}
char *set_origin = sprintf_alloc("$ORIGIN %s%s\n", yp->key,
(yp->key[yp->key_len-1] == '.') ? "" : ".");
if (set_origin == NULL) {
ret = KNOT_ENOMEM;
goto cleanup;
}
knotd_mod_log(mod, LOG_DEBUG, "%s", set_origin);
// Set owner as origin for future record parses.
if (zs_set_input_string(scanner, set_origin, strlen(set_origin)) != 0
|| zs_parse_record(scanner) != 0) {
free(set_origin);
ret = KNOT_EPARSEFAIL;
goto cleanup;
}
free(set_origin);
}
// New geo view description starts.
if (yp->event == YP_EID) {
if (yp->data_len < 4 || yp->data[ISO_CODE_LEN] != ';') {
ret = KNOT_EINVAL;
goto cleanup;
}
// Initialize new geo view.
free(view);
view = malloc(sizeof(geo_view_t));
if (view == NULL) {
ret = KNOT_ENOMEM;
goto cleanup;
}
ret = init_geo_view(view);
if (ret != KNOT_EOK) {
goto cleanup;
}
// Parse geodata/subnet. TODO more generally!
if (ctx->mode == MODE_GEODB) {
size_t copied = strlcpy(view->geodata.country_iso, yp->data, ISO_CODE_LEN + 1);
view->geodata.city_len = yp->data_len - ISO_CODE_LEN - 1;
if ( view->geodata.city_len == 0 ||
(view->geodata.city_len == 1 && yp->data[ISO_CODE_LEN + 1] == '*')) {
view->geodata.city = NULL;
} else {
view->geodata.city = malloc(view->geodata.city_len);
copied = strlcpy(view->geodata.city, yp->data + ISO_CODE_LEN + 1,
view->geodata.city_len);
if (copied != view->geodata.city_len) {
ret = KNOT_EINVAL;
goto cleanup;
}
}
}
if (ctx->mode == MODE_SUBNET) {
char *slash = strchr(yp->data, '/');
view->subnet.prefix = atoi(slash + 1);
*slash = '\0';
inet_pton(AF_INET, yp->data, &view->subnet.addr);
}
}
// Next rrset of the current view.
if (yp->event == YP_EKEY1) {
uint16_t rr_type = KNOT_RRTYPE_A;
if (knot_rrtype_from_string(yp->key, &rr_type) != 0) {
knotd_mod_log(mod, LOG_ERR, "invalid RR type in config");
ret = KNOT_EINVAL;
goto cleanup;
}
knot_rrset_t *add_rr = NULL;
for (size_t i = 0; i < view->count; i++) {
if (view->rrsets[i].type == rr_type) {
add_rr = &view->rrsets[i];
break;
}
}
if (add_rr == NULL) {
if (view->count == view->avail) {
void *alloc_ret = realloc(view->rrsets,
2 * view->avail * sizeof(knot_rrset_t));
if (alloc_ret == NULL) {
ret = KNOT_ENOMEM;
goto cleanup;
}
view->rrsets = alloc_ret;
}
add_rr = &view->rrsets[view->count++];
knot_dname_t *owner_cpy = knot_dname_copy(owner, NULL);
if (owner_cpy == NULL) {
return KNOT_ENOMEM;
}
knot_rrset_init(add_rr, owner_cpy, rr_type, KNOT_CLASS_IN, ctx->ttl);
}
// Parse record.
char *input_string = sprintf_alloc("@ %s %s\n", yp->key, yp->data);
if (input_string == NULL) {
ret = KNOT_ENOMEM;
goto cleanup;
}
knotd_mod_log(mod, LOG_DEBUG, "%s", input_string);
if (zs_set_input_string(scanner, input_string, strlen(input_string)) != 0 ||
zs_parse_record(scanner) != 0 ||
scanner->state != ZS_STATE_DATA) {
free(input_string);
ret = KNOT_EPARSEFAIL;
goto cleanup;
}
free(input_string);
// Add new rdata to current rrset.
ret = knot_rrset_add_rdata(add_rr, scanner->r_data, scanner->r_data_length, NULL);
if (ret != KNOT_EOK) {
goto cleanup;
}
}
}
cleanup:
if (ret != KNOT_EOK) {
clear_geo_view(view);
}
free(view);
zs_deinit(scanner);
free(scanner);
yp_deinit(yp);
free(yp);
return ret;
}
static void clear_geo_trie(trie_t *trie)
{
trie_it_t *it = trie_it_begin(trie);
while (!trie_it_finished(it)) {
geo_trie_val_t *val = (geo_trie_val_t *) (*trie_it_val(it));
for (int i = 0; i < val->count; i++) {
clear_geo_view(&val->views[i]);
}
free(val->views);
free(val);
trie_it_next(it);
}
trie_it_free(it);
trie_clear(trie);
}
void clear_geo_ctx(geoip_ctx_t *ctx)
{
kdnssec_ctx_deinit(&ctx->kctx);
free_zone_keys(&ctx->keyset);
#if HAVE_MAXMINDDB
MMDB_close(&ctx->db);
#endif
clear_geo_trie(ctx->geo_trie);
trie_free(ctx->geo_trie);
}
static knotd_in_state_t geoip_process(knotd_in_state_t state, knot_pkt_t *pkt,
knotd_qdata_t *qdata, knotd_mod_t *mod)
{
assert(pkt && qdata && mod);
geoip_ctx_t *ctx = (geoip_ctx_t *)knotd_mod_ctx(mod);
// Geolocate only A or AAAA records.
uint16_t qtype = knot_pkt_qtype(qdata->query);
if (qtype != KNOT_RRTYPE_A && qtype != KNOT_RRTYPE_AAAA) {
return state;
}
// Check if geolocation is available for given query.
knot_dname_t *qname = knot_pkt_qname(qdata->query);
size_t qname_len = knot_dname_size(qname);
trie_val_t *val = trie_get_try(ctx->geo_trie, (char *)qname, qname_len);
if (val == NULL) {
// Nothing to do in this module.