summaryrefslogtreecommitdiffstats
path: root/lib/dns/geoip2.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/dns/geoip2.c')
-rw-r--r--lib/dns/geoip2.c382
1 files changed, 382 insertions, 0 deletions
diff --git a/lib/dns/geoip2.c b/lib/dns/geoip2.c
new file mode 100644
index 0000000..a3b2206
--- /dev/null
+++ b/lib/dns/geoip2.c
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+/*! \file */
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stdlib.h>
+
+/*
+ * This file is only built and linked if GeoIP2 has been configured.
+ */
+#include <math.h>
+#include <maxminddb.h>
+#include <netinet/in.h>
+
+#include <isc/mem.h>
+#include <isc/once.h>
+#include <isc/sockaddr.h>
+#include <isc/string.h>
+#include <isc/thread.h>
+#include <isc/util.h>
+
+#include <dns/acl.h>
+#include <dns/geoip.h>
+#include <dns/log.h>
+
+/*
+ * This structure preserves state from the previous GeoIP lookup,
+ * so that successive lookups for the same data from the same IP
+ * address will not require repeated database lookups.
+ * This should improve performance somewhat.
+ *
+ * For all lookups we preserve pointers to the MMDB_lookup_result_s
+ * and MMDB_entry_s structures, a pointer to the database from which
+ * the lookup was answered, and a copy of the request address.
+ *
+ * If the next geoip ACL lookup is for the same database and from the
+ * same address, we can reuse the MMDB entry without repeating the lookup.
+ * This is for the case when a single query has to process multiple
+ * geoip ACLs: for example, when there are multiple views with
+ * match-clients statements that search for different countries.
+ *
+ * (XXX: Currently the persistent state is stored in thread specific
+ * memory, but it could more simply be stored in the client object.
+ * Also multiple entries could be stored in case the ACLs require
+ * searching in more than one GeoIP database.)
+ */
+
+typedef struct geoip_state {
+ uint16_t subtype;
+ const MMDB_s *db;
+ isc_netaddr_t addr;
+ MMDB_lookup_result_s mmresult;
+ MMDB_entry_s entry;
+} geoip_state_t;
+
+static thread_local geoip_state_t geoip_state = { 0 };
+
+static void
+set_state(const MMDB_s *db, const isc_netaddr_t *addr,
+ MMDB_lookup_result_s mmresult, MMDB_entry_s entry) {
+ geoip_state.db = db;
+ geoip_state.addr = *addr;
+ geoip_state.mmresult = mmresult;
+ geoip_state.entry = entry;
+}
+
+static geoip_state_t *
+get_entry_for(MMDB_s *const db, const isc_netaddr_t *addr) {
+ isc_sockaddr_t sa;
+ MMDB_lookup_result_s match;
+ int err;
+
+ if (db == geoip_state.db && isc_netaddr_equal(addr, &geoip_state.addr))
+ {
+ return (&geoip_state);
+ }
+
+ isc_sockaddr_fromnetaddr(&sa, addr, 0);
+ match = MMDB_lookup_sockaddr(db, &sa.type.sa, &err);
+ if (err != MMDB_SUCCESS || !match.found_entry) {
+ return (NULL);
+ }
+
+ set_state(db, addr, match, match.entry);
+
+ return (&geoip_state);
+}
+
+static dns_geoip_subtype_t
+fix_subtype(const dns_geoip_databases_t *geoip, dns_geoip_subtype_t subtype) {
+ dns_geoip_subtype_t ret = subtype;
+
+ switch (subtype) {
+ case dns_geoip_countrycode:
+ if (geoip->city != NULL) {
+ ret = dns_geoip_city_countrycode;
+ } else if (geoip->country != NULL) {
+ ret = dns_geoip_country_code;
+ }
+ break;
+ case dns_geoip_countryname:
+ if (geoip->city != NULL) {
+ ret = dns_geoip_city_countryname;
+ } else if (geoip->country != NULL) {
+ ret = dns_geoip_country_name;
+ }
+ break;
+ case dns_geoip_continentcode:
+ if (geoip->city != NULL) {
+ ret = dns_geoip_city_continentcode;
+ } else if (geoip->country != NULL) {
+ ret = dns_geoip_country_continentcode;
+ }
+ break;
+ case dns_geoip_continent:
+ if (geoip->city != NULL) {
+ ret = dns_geoip_city_continent;
+ } else if (geoip->country != NULL) {
+ ret = dns_geoip_country_continent;
+ }
+ break;
+ case dns_geoip_region:
+ if (geoip->city != NULL) {
+ ret = dns_geoip_city_region;
+ }
+ break;
+ case dns_geoip_regionname:
+ if (geoip->city != NULL) {
+ ret = dns_geoip_city_regionname;
+ }
+ default:
+ break;
+ }
+
+ return (ret);
+}
+
+static MMDB_s *
+geoip2_database(const dns_geoip_databases_t *geoip,
+ dns_geoip_subtype_t subtype) {
+ switch (subtype) {
+ case dns_geoip_country_code:
+ case dns_geoip_country_name:
+ case dns_geoip_country_continentcode:
+ case dns_geoip_country_continent:
+ return (geoip->country);
+
+ case dns_geoip_city_countrycode:
+ case dns_geoip_city_countryname:
+ case dns_geoip_city_continentcode:
+ case dns_geoip_city_continent:
+ case dns_geoip_city_region:
+ case dns_geoip_city_regionname:
+ case dns_geoip_city_name:
+ case dns_geoip_city_postalcode:
+ case dns_geoip_city_timezonecode:
+ case dns_geoip_city_metrocode:
+ case dns_geoip_city_areacode:
+ return (geoip->city);
+
+ case dns_geoip_isp_name:
+ return (geoip->isp);
+
+ case dns_geoip_as_asnum:
+ case dns_geoip_org_name:
+ return (geoip->as);
+
+ case dns_geoip_domain_name:
+ return (geoip->domain);
+
+ default:
+ /*
+ * All other subtypes are unavailable in GeoIP2.
+ */
+ return (NULL);
+ }
+}
+
+static bool
+match_string(MMDB_entry_data_s *value, const char *str) {
+ REQUIRE(str != NULL);
+
+ if (value == NULL || !value->has_data ||
+ value->type != MMDB_DATA_TYPE_UTF8_STRING ||
+ value->utf8_string == NULL)
+ {
+ return (false);
+ }
+
+ return (strncasecmp(value->utf8_string, str, value->data_size) == 0);
+}
+
+static bool
+match_int(MMDB_entry_data_s *value, const uint32_t ui32) {
+ if (value == NULL || !value->has_data ||
+ (value->type != MMDB_DATA_TYPE_UINT32 &&
+ value->type != MMDB_DATA_TYPE_UINT16))
+ {
+ return (false);
+ }
+
+ return (value->uint32 == ui32);
+}
+
+bool
+dns_geoip_match(const isc_netaddr_t *reqaddr,
+ const dns_geoip_databases_t *geoip,
+ const dns_geoip_elem_t *elt) {
+ MMDB_s *db = NULL;
+ MMDB_entry_data_s value;
+ geoip_state_t *state = NULL;
+ dns_geoip_subtype_t subtype;
+ const char *s = NULL;
+ int ret;
+
+ REQUIRE(reqaddr != NULL);
+ REQUIRE(elt != NULL);
+ REQUIRE(geoip != NULL);
+
+ subtype = fix_subtype(geoip, elt->subtype);
+ db = geoip2_database(geoip, subtype);
+ if (db == NULL) {
+ return (false);
+ }
+
+ state = get_entry_for(db, reqaddr);
+ if (state == NULL) {
+ return (false);
+ }
+
+ switch (subtype) {
+ case dns_geoip_country_code:
+ case dns_geoip_city_countrycode:
+ ret = MMDB_get_value(&state->entry, &value, "country",
+ "iso_code", (char *)0);
+ if (ret == MMDB_SUCCESS) {
+ return (match_string(&value, elt->as_string));
+ }
+ break;
+
+ case dns_geoip_country_name:
+ case dns_geoip_city_countryname:
+ ret = MMDB_get_value(&state->entry, &value, "country", "names",
+ "en", (char *)0);
+ if (ret == MMDB_SUCCESS) {
+ return (match_string(&value, elt->as_string));
+ }
+ break;
+
+ case dns_geoip_country_continentcode:
+ case dns_geoip_city_continentcode:
+ ret = MMDB_get_value(&state->entry, &value, "continent", "code",
+ (char *)0);
+ if (ret == MMDB_SUCCESS) {
+ return (match_string(&value, elt->as_string));
+ }
+ break;
+
+ case dns_geoip_country_continent:
+ case dns_geoip_city_continent:
+ ret = MMDB_get_value(&state->entry, &value, "continent",
+ "names", "en", (char *)0);
+ if (ret == MMDB_SUCCESS) {
+ return (match_string(&value, elt->as_string));
+ }
+ break;
+
+ case dns_geoip_region:
+ case dns_geoip_city_region:
+ ret = MMDB_get_value(&state->entry, &value, "subdivisions", "0",
+ "iso_code", (char *)0);
+ if (ret == MMDB_SUCCESS) {
+ return (match_string(&value, elt->as_string));
+ }
+ break;
+
+ case dns_geoip_regionname:
+ case dns_geoip_city_regionname:
+ ret = MMDB_get_value(&state->entry, &value, "subdivisions", "0",
+ "names", "en", (char *)0);
+ if (ret == MMDB_SUCCESS) {
+ return (match_string(&value, elt->as_string));
+ }
+ break;
+
+ case dns_geoip_city_name:
+ ret = MMDB_get_value(&state->entry, &value, "city", "names",
+ "en", (char *)0);
+ if (ret == MMDB_SUCCESS) {
+ return (match_string(&value, elt->as_string));
+ }
+ break;
+
+ case dns_geoip_city_postalcode:
+ ret = MMDB_get_value(&state->entry, &value, "postal", "code",
+ (char *)0);
+ if (ret == MMDB_SUCCESS) {
+ return (match_string(&value, elt->as_string));
+ }
+ break;
+
+ case dns_geoip_city_timezonecode:
+ ret = MMDB_get_value(&state->entry, &value, "location",
+ "time_zone", (char *)0);
+ if (ret == MMDB_SUCCESS) {
+ return (match_string(&value, elt->as_string));
+ }
+ break;
+
+ case dns_geoip_city_metrocode:
+ ret = MMDB_get_value(&state->entry, &value, "location",
+ "metro_code", (char *)0);
+ if (ret == MMDB_SUCCESS) {
+ return (match_string(&value, elt->as_string));
+ }
+ break;
+
+ case dns_geoip_isp_name:
+ ret = MMDB_get_value(&state->entry, &value, "isp", (char *)0);
+ if (ret == MMDB_SUCCESS) {
+ return (match_string(&value, elt->as_string));
+ }
+ break;
+
+ case dns_geoip_as_asnum:
+ INSIST(elt->as_string != NULL);
+
+ ret = MMDB_get_value(&state->entry, &value,
+ "autonomous_system_number", (char *)0);
+ if (ret == MMDB_SUCCESS) {
+ int i;
+ s = elt->as_string;
+ if (strncasecmp(s, "AS", 2) == 0) {
+ s += 2;
+ }
+ i = strtol(s, NULL, 10);
+ return (match_int(&value, i));
+ }
+ break;
+
+ case dns_geoip_org_name:
+ ret = MMDB_get_value(&state->entry, &value,
+ "autonomous_system_organization",
+ (char *)0);
+ if (ret == MMDB_SUCCESS) {
+ return (match_string(&value, elt->as_string));
+ }
+ break;
+
+ case dns_geoip_domain_name:
+ ret = MMDB_get_value(&state->entry, &value, "domain",
+ (char *)0);
+ if (ret == MMDB_SUCCESS) {
+ return (match_string(&value, elt->as_string));
+ }
+ break;
+
+ default:
+ /*
+ * For any other subtype, we assume the database was
+ * unavailable and return false.
+ */
+ return (false);
+ }
+
+ /*
+ * No database matched: return false.
+ */
+ return (false);
+}