diff --git a/Makefile.in b/Makefile.in index abff53ead..85287f7ca 100644 --- a/Makefile.in +++ b/Makefile.in @@ -94,6 +94,9 @@ URL=@URL@ GEOIP_CLASSIC_OBJECTS=@GEOIP_CLASSIC_OBJECTS@ GEOIP_CLASSIC_LIBS=@GEOIP_CLASSIC_LIBS@ GEOIP_CLASSIC_CFLAGS=@GEOIP_CLASSIC_CFLAGS@ +GEOIP_MAXMIND_OBJECTS=@GEOIP_MAXMIND_OBJECTS@ +LIBMAXMINDDB_CFLAGS=@LIBMAXMINDDB_CFLAGS@ +LIBMAXMINDDB_LIBS=@LIBMAXMINDDB_LIBS@ # Where is your openssl binary OPENSSLPATH=@OPENSSLPATH@ @@ -123,7 +126,10 @@ MAKEARGS = 'CFLAGS=${CFLAGS}' 'CC=${CC}' 'IRCDLIBS=${IRCDLIBS}' \ 'URL=${URL}' \ 'GEOIP_CLASSIC_OBJECTS=${GEOIP_CLASSIC_OBJECTS}' \ 'GEOIP_CLASSIC_LIBS=${GEOIP_CLASSIC_LIBS}' \ - 'GEOIP_CLASSIC_CFLAGS=${GEOIP_CLASSIC_CFLAGS}' + 'GEOIP_CLASSIC_CFLAGS=${GEOIP_CLASSIC_CFLAGS}' \ + 'GEOIP_MAXMIND_OBJECTS=${GEOIP_MAXMIND_OBJECTS}' \ + 'LIBMAXMINDDB_CFLAGS=${LIBMAXMINDDB_CFLAGS}' \ + 'LIBMAXMINDDB_LIBS=${LIBMAXMINDDB_LIBS}' custommodule: @if test -z "${MODULEFILE}"; then echo "Please set MODULEFILE when calling \`\`make custommodule''. For example, \`\`make custommodule MODULEFILE=callerid''." >&2; exit 1; fi diff --git a/autoconf/m4/unreal.m4 b/autoconf/m4/unreal.m4 index 1d83c0bcb..8b8cf52b8 100644 --- a/autoconf/m4/unreal.m4 +++ b/autoconf/m4/unreal.m4 @@ -382,3 +382,29 @@ AC_DEFUN([CHECK_GEOIP_CLASSIC], ]) dnl AS_IF(enable_geoip_classic) ]) +AC_DEFUN([CHECK_LIBMAXMINDDB], +[ + AC_ARG_ENABLE(libmaxminddb, + [AC_HELP_STRING([--enable-libmaxminddb=no/yes],[enable GeoIP libmaxminddb support])], + [enable_libmaxminddb=$enableval], + [enable_libmaxminddb=yes]) + + AS_IF([test "x$enable_libmaxminddb" = "xyes"], + [ + dnl see if the system provides it + has_system_libmaxminddb="no" + PKG_CHECK_MODULES([LIBMAXMINDDB], [libmaxminddb >= 1.6.0], + [has_system_libmaxminddb=yes], + [has_system_libmaxminddb=no]) + AS_IF([test "x$has_system_libmaxminddb" = "xyes"], + [ + + AC_SUBST(LIBMAXMINDDB_LIBS) + AC_SUBST(LIBMAXMINDDB_CFLAGS) + + GEOIP_MAXMIND_OBJECTS="geoip_maxmind.so" + AC_SUBST(GEOIP_MAXMIND_OBJECTS) + ]) + ]) +]) + diff --git a/configure b/configure index 58c5eea78..65a235cbd 100755 --- a/configure +++ b/configure @@ -626,6 +626,9 @@ ac_subst_vars='LTLIBOBJS LIBOBJS UNRLINCDIR IRCDLIBS +GEOIP_MAXMIND_OBJECTS +LIBMAXMINDDB_LIBS +LIBMAXMINDDB_CFLAGS GEOIP_CLASSIC_OBJECTS GEOIP_CLASSIC_LIBS GEOIP_CLASSIC_CFLAGS @@ -767,6 +770,7 @@ enable_werror enable_asan enable_libcurl enable_geoip_classic +enable_libmaxminddb ' ac_precious_vars='build_alias host_alias @@ -791,7 +795,9 @@ CARES_LIBS JANSSON_CFLAGS JANSSON_LIBS GEOIP_CLASSIC_CFLAGS -GEOIP_CLASSIC_LIBS' +GEOIP_CLASSIC_LIBS +LIBMAXMINDDB_CFLAGS +LIBMAXMINDDB_LIBS' # Initialize some variables set by options. @@ -1432,6 +1438,8 @@ Optional Features: --enable-libcurl=DIR enable libcurl (remote include) support --enable-geoip-classic=no/yes enable GeoIP Classic support + --enable-libmaxminddb=no/yes + enable GeoIP libmaxminddb support Optional Packages: --with-PACKAGE[=ARG] use PACKAGE [ARG=yes] @@ -1511,6 +1519,10 @@ Some influential environment variables: C compiler flags for GEOIP_CLASSIC, overriding pkg-config GEOIP_CLASSIC_LIBS linker flags for GEOIP_CLASSIC, overriding pkg-config + LIBMAXMINDDB_CFLAGS + C compiler flags for LIBMAXMINDDB, overriding pkg-config + LIBMAXMINDDB_LIBS + linker flags for LIBMAXMINDDB, overriding pkg-config Use these variables to override the choices made by `configure' or to help it to find libraries and programs with nonstandard names/locations. @@ -8695,6 +8707,103 @@ fi fi + + # Check whether --enable-libmaxminddb was given. +if test "${enable_libmaxminddb+set}" = set; then : + enableval=$enable_libmaxminddb; enable_libmaxminddb=$enableval +else + enable_libmaxminddb=yes +fi + + + if test "x$enable_libmaxminddb" = "xyes"; then : + + has_system_libmaxminddb="no" + +pkg_failed=no +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for libmaxminddb >= 1.6.0" >&5 +$as_echo_n "checking for libmaxminddb >= 1.6.0... " >&6; } + +if test -n "$LIBMAXMINDDB_CFLAGS"; then + pkg_cv_LIBMAXMINDDB_CFLAGS="$LIBMAXMINDDB_CFLAGS" + elif test -n "$PKG_CONFIG"; then + if test -n "$PKG_CONFIG" && \ + { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libmaxminddb >= 1.6.0\""; } >&5 + ($PKG_CONFIG --exists --print-errors "libmaxminddb >= 1.6.0") 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; }; then + pkg_cv_LIBMAXMINDDB_CFLAGS=`$PKG_CONFIG --cflags "libmaxminddb >= 1.6.0" 2>/dev/null` + test "x$?" != "x0" && pkg_failed=yes +else + pkg_failed=yes +fi + else + pkg_failed=untried +fi +if test -n "$LIBMAXMINDDB_LIBS"; then + pkg_cv_LIBMAXMINDDB_LIBS="$LIBMAXMINDDB_LIBS" + elif test -n "$PKG_CONFIG"; then + if test -n "$PKG_CONFIG" && \ + { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libmaxminddb >= 1.6.0\""; } >&5 + ($PKG_CONFIG --exists --print-errors "libmaxminddb >= 1.6.0") 2>&5 + ac_status=$? + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; }; then + pkg_cv_LIBMAXMINDDB_LIBS=`$PKG_CONFIG --libs "libmaxminddb >= 1.6.0" 2>/dev/null` + test "x$?" != "x0" && pkg_failed=yes +else + pkg_failed=yes +fi + else + pkg_failed=untried +fi + + + +if test $pkg_failed = yes; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } + +if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then + _pkg_short_errors_supported=yes +else + _pkg_short_errors_supported=no +fi + if test $_pkg_short_errors_supported = yes; then + LIBMAXMINDDB_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libmaxminddb >= 1.6.0" 2>&1` + else + LIBMAXMINDDB_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libmaxminddb >= 1.6.0" 2>&1` + fi + # Put the nasty error message in config.log where it belongs + echo "$LIBMAXMINDDB_PKG_ERRORS" >&5 + + has_system_libmaxminddb=no +elif test $pkg_failed = untried; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } + has_system_libmaxminddb=no +else + LIBMAXMINDDB_CFLAGS=$pkg_cv_LIBMAXMINDDB_CFLAGS + LIBMAXMINDDB_LIBS=$pkg_cv_LIBMAXMINDDB_LIBS + { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +$as_echo "yes" >&6; } + has_system_libmaxminddb=yes +fi + if test "x$has_system_libmaxminddb" = "xyes"; then : + + + + + + GEOIP_MAXMIND_OBJECTS="geoip_maxmind.so" + + +fi + +fi + + UNRLINCDIR="`pwd`/include" if test "$ac_cv_werror" = "yes" ; then diff --git a/configure.ac b/configure.ac index c7ab945ab..8fc5a918e 100644 --- a/configure.ac +++ b/configure.ac @@ -833,6 +833,8 @@ CHECK_LIBCURL CHECK_GEOIP_CLASSIC +CHECK_LIBMAXMINDDB + UNRLINCDIR="`pwd`/include" dnl Moved to the very end to ensure it doesn't affect any libs or tests. diff --git a/src/modules/Makefile.in b/src/modules/Makefile.in index f6d68398d..cf23acc2c 100644 --- a/src/modules/Makefile.in +++ b/src/modules/Makefile.in @@ -78,7 +78,8 @@ R_MODULES= \ monitor.so slog.so tls_cipher.so \ unreal_server_compat.so metadata.so \ extended-monitor.so geoip_csv.so \ - geoip_base.so $(GEOIP_CLASSIC_OBJECTS) + geoip_base.so \ + $(GEOIP_CLASSIC_OBJECTS) $(GEOIP_MAXMIND_OBJECTS) MODULES=oldcloak.so $(R_MODULES) MODULEFLAGS=@MODULEFLAGS@ @@ -701,6 +702,10 @@ geoip_csv.so: geoip_csv.c $(INCLUDES) $(CC) $(CFLAGS) $(MODULEFLAGS) -DDYNAMIC_LINKING \ -o geoip_csv.so geoip_csv.c +geoip_maxmind.so: geoip_maxmind.c $(INCLUDES) + $(CC) $(CFLAGS) $(MODULEFLAGS) $(LIBMAXMINDDB_CFLAGS) $(LIBMAXMINDDB_LIBS) -DDYNAMIC_LINKING \ + -o geoip_maxmind.so geoip_maxmind.c + ############################################################################# # capabilities ############################################################################# diff --git a/src/modules/geoip_maxmind.c b/src/modules/geoip_maxmind.c new file mode 100644 index 000000000..c987a309b --- /dev/null +++ b/src/modules/geoip_maxmind.c @@ -0,0 +1,239 @@ +/* GEOIP maxmind module + * (C) Copyright 2021 Bram Matthys and the UnrealIRCd team + * License: GPLv2 + */ + +#include "unrealircd.h" +#include + +ModuleHeader MOD_HEADER + = { + "geoip_maxmind", + "5.0", + "GEOIP using maxmind databases", + "UnrealIRCd Team", + "unrealircd-6", + }; + +struct geoip_maxmind_config_s { + char *db_file; +/* for config reading only */ + int have_config; + int have_database; +}; + +/* Variables */ + +struct geoip_maxmind_config_s geoip_maxmind_config; +MMDB_s mmdb; + +/* Forward declarations */ +int geoip_maxmind_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs); +int geoip_maxmind_configposttest(int *errs); +int geoip_maxmind_configrun(ConfigFile *cf, ConfigEntry *ce, int type); +void geoip_maxmind_free(void); +GeoIPResult *geoip_lookup_maxmind(char *ip); + +int geoip_maxmind_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs) +{ + ConfigEntry *cep; + int errors = 0; + int i; + + if (type != CONFIG_SET) + return 0; + + if (!ce || !ce->name) + return 0; + + if (strcmp(ce->name, "geoip-maxmind")) + return 0; + + geoip_maxmind_config.have_config = 1; + + for (cep = ce->items; cep; cep = cep->next) + { + if (!strcmp(cep->name, "database")) + { + if (geoip_maxmind_config.have_database) + { + config_error("%s:%i: duplicate item set::geoip-maxmind::%s", cep->file->filename, cep->line_number, cep->name); + continue; + } + if (!is_file_readable(cep->value, PERMDATADIR)) + { + config_error("%s:%i: set::geoip-maxmind::%s: cannot open file \"%s/%s\" for reading (%s)", cep->file->filename, cep->line_number, cep->name, PERMDATADIR, cep->value, strerror(errno)); + errors++; + continue; + } + geoip_maxmind_config.have_database = 1; + continue; + } + config_warn("%s:%i: unknown item set::geoip-maxmind::%s", cep->file->filename, cep->line_number, cep->name); + } + + *errs = errors; + return errors ? -1 : 1; +} + +int geoip_maxmind_configposttest(int *errs) +{ + int errors = 0; + if (geoip_maxmind_config.have_config) + { + if (!geoip_maxmind_config.have_database) + { + config_error("geoip_maxmind: no database file specified! Remove set::geoip-maxmind to use defaults"); + errors++; + } + } else + { + safe_strdup(geoip_maxmind_config.db_file, "GeoLite2-Country.mmdb"); + + if (is_file_readable(geoip_maxmind_config.db_file, PERMDATADIR)) + { + geoip_maxmind_config.have_database = 1; + } else + { + config_error("[geoip_maxmind] cannot open database file \"%s/%s\" for reading (%s)", PERMDATADIR, geoip_maxmind_config.db_file, strerror(errno)); + safe_free(geoip_maxmind_config.db_file); + errors++; + } + } + + *errs = errors; + return errors ? -1 : 1; +} + +int geoip_maxmind_configrun(ConfigFile *cf, ConfigEntry *ce, int type) +{ + ConfigEntry *cep; + + if (type != CONFIG_SET) + return 0; + + if (!ce || !ce->name) + return 0; + + if (strcmp(ce->name, "geoip-maxmind")) + return 0; + + for (cep = ce->items; cep; cep = cep->next) + { + if (!strcmp(cep->name, "database") && geoip_maxmind_config.have_database) + safe_strdup(geoip_maxmind_config.db_file, cep->value); + } + return 1; +} + +MOD_TEST() +{ + MARK_AS_OFFICIAL_MODULE(modinfo); + if (!CallbackAddPVoidEx(modinfo->handle, CALLBACKTYPE_GEOIP_LOOKUP, TO_PVOIDFUNC(geoip_lookup_maxmind))) + { + unreal_log(ULOG_ERROR, "geoip_maxmind", "GEOIP_ADD_CALLBACK_FAILED", NULL, + "geoip_maxmind: Could not install GEOIP_LOOKUP callback. " + "Most likely another geoip module is already loaded. " + "You can only load one!"); + return MOD_FAILED; + } + + geoip_maxmind_config.have_config = 0; + geoip_maxmind_config.have_database = 0; + HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, geoip_maxmind_configtest); + HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, geoip_maxmind_configposttest); + return MOD_SUCCESS; +} + +MOD_INIT() +{ + MARK_AS_OFFICIAL_MODULE(modinfo); + HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, geoip_maxmind_configrun); + return MOD_SUCCESS; +} + +MOD_LOAD() +{ + geoip_maxmind_free(); + convert_to_absolute_path(&geoip_maxmind_config.db_file, PERMDATADIR); + + int status = MMDB_open(geoip_maxmind_config.db_file, MMDB_MODE_MMAP, &mmdb); + + if (status != MMDB_SUCCESS) { + int save_err = errno; + unreal_log(ULOG_WARNING, "geoip_maxmind", "GEOIP_CANNOT_OPEN_DB", NULL, + "Could not open '$filename' - $maxmind_error; IO error: $io_error", + log_data_string("filename", geoip_maxmind_config.db_file), + log_data_string("maxmind_error", MMDB_strerror(status)), + log_data_string("io_error", (status == MMDB_IO_ERROR)?strerror(save_err):"none")); + return MOD_FAILED; + } + return MOD_SUCCESS; +} + +MOD_UNLOAD() +{ + geoip_maxmind_free(); + return MOD_SUCCESS; +} + +void geoip_maxmind_free(void) +{ + MMDB_close(&mmdb); +} + +GeoIPResult *geoip_lookup_maxmind(char *ip) +{ + int gai_error, mmdb_error, status; + MMDB_lookup_result_s result; + MMDB_entry_data_s country_code; + MMDB_entry_data_s country_name; + char *country_code_str, *country_name_str; + GeoIPResult *r; + + if (!ip) + return NULL; + + result = MMDB_lookup_string(&mmdb, ip, &gai_error, &mmdb_error); + if (gai_error) + { + unreal_log(ULOG_DEBUG, "geoip_maxmind", "GEOIP_DB_ERROR", NULL, + "libmaxminddb: getaddrinfo error for $ip: $error", + log_data_string("ip", ip), + log_data_string("error", gai_strerror(gai_error))); + return NULL; + } + + if (mmdb_error != MMDB_SUCCESS) + { + unreal_log(ULOG_DEBUG, "geoip_maxmind", "GEOIP_DB_ERROR", NULL, + "libmaxminddb: library error for $ip: $error", + log_data_string("ip", ip), + log_data_string("error", MMDB_strerror(mmdb_error))); + return NULL; + } + + if (!result.found_entry) /* no result */ + return NULL; + + status = MMDB_get_value(&result.entry, &country_code, "country", "iso_code", NULL); + if (status != MMDB_SUCCESS || !country_code.has_data || country_code.type != MMDB_DATA_TYPE_UTF8_STRING) + return NULL; + status = MMDB_get_value(&result.entry, &country_name, "country", "names", "en", NULL); + if (status != MMDB_SUCCESS || !country_name.has_data || country_name.type != MMDB_DATA_TYPE_UTF8_STRING) + return NULL; + + /* these results are not null-terminated */ + country_code_str = safe_alloc(country_code.data_size + 1); + country_name_str = safe_alloc(country_name.data_size + 1); + memcpy(country_code_str, country_code.utf8_string, country_code.data_size); + country_code_str[country_code.data_size] = '\0'; + memcpy(country_name_str, country_name.utf8_string, country_name.data_size); + country_name_str[country_name.data_size] = '\0'; + + r = safe_alloc(sizeof(GeoIPResult)); + r->country_code = country_code_str; + r->country_name = country_name_str; + return r; +} +