1/*	$NetBSD: geoip2.c,v 1.7 2024/02/21 22:52:06 christos Exp $	*/
2
3/*
4 * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
5 *
6 * SPDX-License-Identifier: MPL-2.0
7 *
8 * This Source Code Form is subject to the terms of the Mozilla Public
9 * License, v. 2.0. If a copy of the MPL was not distributed with this
10 * file, you can obtain one at https://mozilla.org/MPL/2.0/.
11 *
12 * See the COPYRIGHT file distributed with this work for additional
13 * information regarding copyright ownership.
14 */
15
16/*! \file */
17
18#include <inttypes.h>
19#include <stdbool.h>
20#include <stdlib.h>
21
22/*
23 * This file is only built and linked if GeoIP2 has been configured.
24 */
25#include <math.h>
26#include <maxminddb.h>
27#include <netinet/in.h>
28
29#include <isc/mem.h>
30#include <isc/once.h>
31#include <isc/sockaddr.h>
32#include <isc/string.h>
33#include <isc/thread.h>
34#include <isc/util.h>
35
36#include <dns/acl.h>
37#include <dns/geoip.h>
38#include <dns/log.h>
39
40/*
41 * This structure preserves state from the previous GeoIP lookup,
42 * so that successive lookups for the same data from the same IP
43 * address will not require repeated database lookups.
44 * This should improve performance somewhat.
45 *
46 * For all lookups we preserve pointers to the MMDB_lookup_result_s
47 * and MMDB_entry_s structures, a pointer to the database from which
48 * the lookup was answered, and a copy of the request address.
49 *
50 * If the next geoip ACL lookup is for the same database and from the
51 * same address, we can reuse the MMDB entry without repeating the lookup.
52 * This is for the case when a single query has to process multiple
53 * geoip ACLs: for example, when there are multiple views with
54 * match-clients statements that search for different countries.
55 *
56 * (XXX: Currently the persistent state is stored in thread specific
57 * memory, but it could more simply be stored in the client object.
58 * Also multiple entries could be stored in case the ACLs require
59 * searching in more than one GeoIP database.)
60 */
61
62typedef struct geoip_state {
63	uint16_t subtype;
64	const MMDB_s *db;
65	isc_netaddr_t addr;
66	MMDB_lookup_result_s mmresult;
67	MMDB_entry_s entry;
68} geoip_state_t;
69
70static thread_local geoip_state_t geoip_state = { 0 };
71
72static void
73set_state(const MMDB_s *db, const isc_netaddr_t *addr,
74	  MMDB_lookup_result_s mmresult, MMDB_entry_s entry) {
75	geoip_state.db = db;
76	geoip_state.addr = *addr;
77	geoip_state.mmresult = mmresult;
78	geoip_state.entry = entry;
79}
80
81static geoip_state_t *
82get_entry_for(MMDB_s *const db, const isc_netaddr_t *addr) {
83	isc_sockaddr_t sa;
84	MMDB_lookup_result_s match;
85	int err;
86
87	if (db == geoip_state.db && isc_netaddr_equal(addr, &geoip_state.addr))
88	{
89		return (&geoip_state);
90	}
91
92	isc_sockaddr_fromnetaddr(&sa, addr, 0);
93	match = MMDB_lookup_sockaddr(db, &sa.type.sa, &err);
94	if (err != MMDB_SUCCESS || !match.found_entry) {
95		return (NULL);
96	}
97
98	set_state(db, addr, match, match.entry);
99
100	return (&geoip_state);
101}
102
103static dns_geoip_subtype_t
104fix_subtype(const dns_geoip_databases_t *geoip, dns_geoip_subtype_t subtype) {
105	dns_geoip_subtype_t ret = subtype;
106
107	switch (subtype) {
108	case dns_geoip_countrycode:
109		if (geoip->city != NULL) {
110			ret = dns_geoip_city_countrycode;
111		} else if (geoip->country != NULL) {
112			ret = dns_geoip_country_code;
113		}
114		break;
115	case dns_geoip_countryname:
116		if (geoip->city != NULL) {
117			ret = dns_geoip_city_countryname;
118		} else if (geoip->country != NULL) {
119			ret = dns_geoip_country_name;
120		}
121		break;
122	case dns_geoip_continentcode:
123		if (geoip->city != NULL) {
124			ret = dns_geoip_city_continentcode;
125		} else if (geoip->country != NULL) {
126			ret = dns_geoip_country_continentcode;
127		}
128		break;
129	case dns_geoip_continent:
130		if (geoip->city != NULL) {
131			ret = dns_geoip_city_continent;
132		} else if (geoip->country != NULL) {
133			ret = dns_geoip_country_continent;
134		}
135		break;
136	case dns_geoip_region:
137		if (geoip->city != NULL) {
138			ret = dns_geoip_city_region;
139		}
140		break;
141	case dns_geoip_regionname:
142		if (geoip->city != NULL) {
143			ret = dns_geoip_city_regionname;
144		}
145	default:
146		break;
147	}
148
149	return (ret);
150}
151
152static MMDB_s *
153geoip2_database(const dns_geoip_databases_t *geoip,
154		dns_geoip_subtype_t subtype) {
155	switch (subtype) {
156	case dns_geoip_country_code:
157	case dns_geoip_country_name:
158	case dns_geoip_country_continentcode:
159	case dns_geoip_country_continent:
160		return (geoip->country);
161
162	case dns_geoip_city_countrycode:
163	case dns_geoip_city_countryname:
164	case dns_geoip_city_continentcode:
165	case dns_geoip_city_continent:
166	case dns_geoip_city_region:
167	case dns_geoip_city_regionname:
168	case dns_geoip_city_name:
169	case dns_geoip_city_postalcode:
170	case dns_geoip_city_timezonecode:
171	case dns_geoip_city_metrocode:
172	case dns_geoip_city_areacode:
173		return (geoip->city);
174
175	case dns_geoip_isp_name:
176		return (geoip->isp);
177
178	case dns_geoip_as_asnum:
179	case dns_geoip_org_name:
180		return (geoip->as);
181
182	case dns_geoip_domain_name:
183		return (geoip->domain);
184
185	default:
186		/*
187		 * All other subtypes are unavailable in GeoIP2.
188		 */
189		return (NULL);
190	}
191}
192
193static bool
194match_string(MMDB_entry_data_s *value, const char *str) {
195	REQUIRE(str != NULL);
196
197	if (value == NULL || !value->has_data ||
198	    value->type != MMDB_DATA_TYPE_UTF8_STRING ||
199	    value->utf8_string == NULL)
200	{
201		return (false);
202	}
203
204	return (strncasecmp(value->utf8_string, str, value->data_size) == 0);
205}
206
207static bool
208match_int(MMDB_entry_data_s *value, const uint32_t ui32) {
209	if (value == NULL || !value->has_data ||
210	    (value->type != MMDB_DATA_TYPE_UINT32 &&
211	     value->type != MMDB_DATA_TYPE_UINT16))
212	{
213		return (false);
214	}
215
216	return (value->uint32 == ui32);
217}
218
219bool
220dns_geoip_match(const isc_netaddr_t *reqaddr,
221		const dns_geoip_databases_t *geoip,
222		const dns_geoip_elem_t *elt) {
223	MMDB_s *db = NULL;
224	MMDB_entry_data_s value;
225	geoip_state_t *state = NULL;
226	dns_geoip_subtype_t subtype;
227	const char *s = NULL;
228	int ret;
229
230	REQUIRE(reqaddr != NULL);
231	REQUIRE(elt != NULL);
232	REQUIRE(geoip != NULL);
233
234	subtype = fix_subtype(geoip, elt->subtype);
235	db = geoip2_database(geoip, subtype);
236	if (db == NULL) {
237		return (false);
238	}
239
240	state = get_entry_for(db, reqaddr);
241	if (state == NULL) {
242		return (false);
243	}
244
245	switch (subtype) {
246	case dns_geoip_country_code:
247	case dns_geoip_city_countrycode:
248		ret = MMDB_get_value(&state->entry, &value, "country",
249				     "iso_code", (char *)0);
250		if (ret == MMDB_SUCCESS) {
251			return (match_string(&value, elt->as_string));
252		}
253		break;
254
255	case dns_geoip_country_name:
256	case dns_geoip_city_countryname:
257		ret = MMDB_get_value(&state->entry, &value, "country", "names",
258				     "en", (char *)0);
259		if (ret == MMDB_SUCCESS) {
260			return (match_string(&value, elt->as_string));
261		}
262		break;
263
264	case dns_geoip_country_continentcode:
265	case dns_geoip_city_continentcode:
266		ret = MMDB_get_value(&state->entry, &value, "continent", "code",
267				     (char *)0);
268		if (ret == MMDB_SUCCESS) {
269			return (match_string(&value, elt->as_string));
270		}
271		break;
272
273	case dns_geoip_country_continent:
274	case dns_geoip_city_continent:
275		ret = MMDB_get_value(&state->entry, &value, "continent",
276				     "names", "en", (char *)0);
277		if (ret == MMDB_SUCCESS) {
278			return (match_string(&value, elt->as_string));
279		}
280		break;
281
282	case dns_geoip_region:
283	case dns_geoip_city_region:
284		ret = MMDB_get_value(&state->entry, &value, "subdivisions", "0",
285				     "iso_code", (char *)0);
286		if (ret == MMDB_SUCCESS) {
287			return (match_string(&value, elt->as_string));
288		}
289		break;
290
291	case dns_geoip_regionname:
292	case dns_geoip_city_regionname:
293		ret = MMDB_get_value(&state->entry, &value, "subdivisions", "0",
294				     "names", "en", (char *)0);
295		if (ret == MMDB_SUCCESS) {
296			return (match_string(&value, elt->as_string));
297		}
298		break;
299
300	case dns_geoip_city_name:
301		ret = MMDB_get_value(&state->entry, &value, "city", "names",
302				     "en", (char *)0);
303		if (ret == MMDB_SUCCESS) {
304			return (match_string(&value, elt->as_string));
305		}
306		break;
307
308	case dns_geoip_city_postalcode:
309		ret = MMDB_get_value(&state->entry, &value, "postal", "code",
310				     (char *)0);
311		if (ret == MMDB_SUCCESS) {
312			return (match_string(&value, elt->as_string));
313		}
314		break;
315
316	case dns_geoip_city_timezonecode:
317		ret = MMDB_get_value(&state->entry, &value, "location",
318				     "time_zone", (char *)0);
319		if (ret == MMDB_SUCCESS) {
320			return (match_string(&value, elt->as_string));
321		}
322		break;
323
324	case dns_geoip_city_metrocode:
325		ret = MMDB_get_value(&state->entry, &value, "location",
326				     "metro_code", (char *)0);
327		if (ret == MMDB_SUCCESS) {
328			return (match_string(&value, elt->as_string));
329		}
330		break;
331
332	case dns_geoip_isp_name:
333		ret = MMDB_get_value(&state->entry, &value, "isp", (char *)0);
334		if (ret == MMDB_SUCCESS) {
335			return (match_string(&value, elt->as_string));
336		}
337		break;
338
339	case dns_geoip_as_asnum:
340		INSIST(elt->as_string != NULL);
341
342		ret = MMDB_get_value(&state->entry, &value,
343				     "autonomous_system_number", (char *)0);
344		if (ret == MMDB_SUCCESS) {
345			int i;
346			s = elt->as_string;
347			if (strncasecmp(s, "AS", 2) == 0) {
348				s += 2;
349			}
350			i = strtol(s, NULL, 10);
351			return (match_int(&value, i));
352		}
353		break;
354
355	case dns_geoip_org_name:
356		ret = MMDB_get_value(&state->entry, &value,
357				     "autonomous_system_organization",
358				     (char *)0);
359		if (ret == MMDB_SUCCESS) {
360			return (match_string(&value, elt->as_string));
361		}
362		break;
363
364	case dns_geoip_domain_name:
365		ret = MMDB_get_value(&state->entry, &value, "domain",
366				     (char *)0);
367		if (ret == MMDB_SUCCESS) {
368			return (match_string(&value, elt->as_string));
369		}
370		break;
371
372	default:
373		/*
374		 * For any other subtype, we assume the database was
375		 * unavailable and return false.
376		 */
377		return (false);
378	}
379
380	/*
381	 * No database matched: return false.
382	 */
383	return (false);
384}
385