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