diff --git a/Makefile.in b/Makefile.in index 1028f97be..25083ef0b 100644 --- a/Makefile.in +++ b/Makefile.in @@ -94,9 +94,6 @@ 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@ @@ -126,10 +123,7 @@ 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_MAXMIND_OBJECTS=${GEOIP_MAXMIND_OBJECTS}' \ - 'LIBMAXMINDDB_CFLAGS=${LIBMAXMINDDB_CFLAGS}' \ - 'LIBMAXMINDDB_LIBS=${LIBMAXMINDDB_LIBS}' + 'GEOIP_CLASSIC_CFLAGS=${GEOIP_CLASSIC_CFLAGS}' 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/Makefile.windows b/Makefile.windows index f9d005053..f8b0d59f7 100644 --- a/Makefile.windows +++ b/Makefile.windows @@ -275,6 +275,7 @@ DLL_FILES=\ src/modules/geoip_base.dll \ src/modules/geoip_classic.dll \ src/modules/geoip_csv.dll \ + src/modules/geoip_maxmind.dll \ src/modules/geoip-tag.dll \ src/modules/globops.dll \ src/modules/help.dll \ @@ -966,12 +967,12 @@ src/modules/geoip_classic.dll: src/modules/geoip_classic.c $(INCLUDES) src/modules/geoip_csv.dll: src/modules/geoip_csv.c $(INCLUDES) $(CC) $(MODCFLAGS) src/modules/geoip_csv.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/geoip_csv.pdb $(MODLFLAGS) +src/modules/geoip_maxmind.dll: src/modules/geoip_maxmind.c $(INCLUDES) + $(CC) $(MODCFLAGS) src/modules/geoip_maxmind.c src/modules/mmdb.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/geoip_maxmind.pdb $(MODLFLAGS) + src/modules/geoip-tag.dll: src/modules/geoip-tag.c $(INCLUDES) $(CC) $(MODCFLAGS) src/modules/geoip-tag.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/geoip-tag.pdb $(MODLFLAGS) -src/modules/geoip_maxmind.dll: src/modules/geoip_maxmind.c $(INCLUDES) - $(CC) $(MODCFLAGS) src/modules/geoip_maxmind.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/geoip_maxmind.pdb $(MODLFLAGS) - src/modules/globops.dll: src/modules/globops.c $(INCLUDES) $(CC) $(MODCFLAGS) src/modules/globops.c /Fesrc/modules/ /Fosrc/modules/ /Fdsrc/modules/globops.pdb $(MODLFLAGS) diff --git a/autoconf/m4/unreal.m4 b/autoconf/m4/unreal.m4 index 6d8da8000..69899da97 100644 --- a/autoconf/m4/unreal.m4 +++ b/autoconf/m4/unreal.m4 @@ -469,29 +469,3 @@ AC_DEFUN([CHECK_GEOIP_CLASSIC], AC_SUBST(GEOIP_CLASSIC_OBJECTS) ]) 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=no]) - - AS_IF([test "x$enable_libmaxminddb" = "xyes"], - [ - dnl see if the system provides it - has_system_libmaxminddb="no" - PKG_CHECK_MODULES([LIBMAXMINDDB], [libmaxminddb >= 1.4.3], - [has_system_libmaxminddb=yes]) - 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 cd3001c0b..6618a2d4d 100755 --- a/configure +++ b/configure @@ -646,9 +646,6 @@ ac_subst_vars='LTLIBOBJS LIBOBJS UNRLINCDIR IRCDLIBS -GEOIP_MAXMIND_OBJECTS -LIBMAXMINDDB_LIBS -LIBMAXMINDDB_CFLAGS GEOIP_CLASSIC_OBJECTS GEOIP_CLASSIC_LIBS GEOIP_CLASSIC_CFLAGS @@ -787,7 +784,6 @@ enable_werror enable_asan enable_libcurl enable_geoip_classic -enable_libmaxminddb ' ac_precious_vars='build_alias host_alias @@ -812,9 +808,7 @@ CARES_LIBS JANSSON_CFLAGS JANSSON_LIBS GEOIP_CLASSIC_CFLAGS -GEOIP_CLASSIC_LIBS -LIBMAXMINDDB_CFLAGS -LIBMAXMINDDB_LIBS' +GEOIP_CLASSIC_LIBS' # Initialize some variables set by options. @@ -1452,8 +1446,6 @@ 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] @@ -1534,10 +1526,6 @@ 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. @@ -10147,127 +10135,6 @@ fi fi - - # Check whether --enable-libmaxminddb was given. -if test ${enable_libmaxminddb+y} -then : - enableval=$enable_libmaxminddb; enable_libmaxminddb=$enableval -else case e in #( - e) enable_libmaxminddb=no ;; -esac -fi - - - if test "x$enable_libmaxminddb" = "xyes" -then : - - has_system_libmaxminddb="no" - -pkg_failed=no -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for libmaxminddb >= 1.4.3" >&5 -printf %s "checking for libmaxminddb >= 1.4.3... " >&6; } - -if test -n "$LIBMAXMINDDB_CFLAGS"; then - pkg_cv_LIBMAXMINDDB_CFLAGS="$LIBMAXMINDDB_CFLAGS" - elif test -n "$PKG_CONFIG"; then - if test -n "$PKG_CONFIG" && \ - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libmaxminddb >= 1.4.3\""; } >&5 - ($PKG_CONFIG --exists --print-errors "libmaxminddb >= 1.4.3") 2>&5 - ac_status=$? - printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 - test $ac_status = 0; }; then - pkg_cv_LIBMAXMINDDB_CFLAGS=`$PKG_CONFIG --cflags "libmaxminddb >= 1.4.3" 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" && \ - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libmaxminddb >= 1.4.3\""; } >&5 - ($PKG_CONFIG --exists --print-errors "libmaxminddb >= 1.4.3") 2>&5 - ac_status=$? - printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 - test $ac_status = 0; }; then - pkg_cv_LIBMAXMINDDB_LIBS=`$PKG_CONFIG --libs "libmaxminddb >= 1.4.3" 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 - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "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.4.3" 2>&1` - else - LIBMAXMINDDB_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libmaxminddb >= 1.4.3" 2>&1` - fi - # Put the nasty error message in config.log where it belongs - echo "$LIBMAXMINDDB_PKG_ERRORS" >&5 - - as_fn_error $? "Package requirements (libmaxminddb >= 1.4.3) were not met: - -$LIBMAXMINDDB_PKG_ERRORS - -Consider adjusting the PKG_CONFIG_PATH environment variable if you -installed software in a non-standard prefix. - -Alternatively, you may set the environment variables LIBMAXMINDDB_CFLAGS -and LIBMAXMINDDB_LIBS to avoid the need to call pkg-config. -See the pkg-config man page for more details." "$LINENO" 5 -elif test $pkg_failed = untried; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 -printf "%s\n" "no" >&6; } - { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':" >&5 -printf "%s\n" "$as_me: error: in '$ac_pwd':" >&2;} -as_fn_error $? "The pkg-config script could not be found or is too old. Make sure it -is in your PATH or set the PKG_CONFIG environment variable to the full -path to pkg-config. - -Alternatively, you may set the environment variables LIBMAXMINDDB_CFLAGS -and LIBMAXMINDDB_LIBS to avoid the need to call pkg-config. -See the pkg-config man page for more details. - -To get pkg-config, see . -See 'config.log' for more details" "$LINENO" 5; } -else - LIBMAXMINDDB_CFLAGS=$pkg_cv_LIBMAXMINDDB_CFLAGS - LIBMAXMINDDB_LIBS=$pkg_cv_LIBMAXMINDDB_LIBS - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -printf "%s\n" "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" { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking if explicit -std=gnu17 is needed" >&5 diff --git a/configure.ac b/configure.ac index f46390c93..a678f67db 100644 --- a/configure.ac +++ b/configure.ac @@ -930,8 +930,6 @@ CHECK_LIBCURL CHECK_GEOIP_CLASSIC -CHECK_LIBMAXMINDDB - UNRLINCDIR="`pwd`/include" dnl This is at the end so the (potential) -std=gnu17 is not used diff --git a/include/mmdb.h b/include/mmdb.h new file mode 100644 index 000000000..f9d0e3dfa --- /dev/null +++ b/include/mmdb.h @@ -0,0 +1,170 @@ +/* + * mmdb.h - Minimal MMDB (MaxMind DB) reader library + * + * Written from the MaxMind DB file format specification + * (https://maxmind.github.io/MaxMind-DB/). + * + * This C implementation was written by the UnrealIRCd team, + * using the Go MMDB reader oschwald/maxminddb-golang by + * Gregory J. Oschwald as a reference during development. + * + * Copyright (c) 2015 Gregory J. Oschwald (Go implementation) + * Copyright (c) 2026 UnrealIRCd team + * + * Permission to use, copy, modify, and/or distribute this software for + * any purpose with or without fee is hereby granted, provided that + * the above copyright notice and this permission notice appear in + * all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL + * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE + * AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR + * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, + * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef MMDB_H +#define MMDB_H + +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + +/** Status/error codes returned by mmdb functions */ +typedef enum { + MMDB_OK = 0, /**< Success */ + MMDB_ERR_OPEN, /**< Could not open or mmap the file */ + MMDB_ERR_INVALID_DB, /**< Not a valid MMDB file */ + MMDB_ERR_CORRUPT, /**< Search tree or data section corruption */ + MMDB_ERR_NODATA, /**< IP found but requested path doesn't exist */ + MMDB_ERR_TYPE, /**< Type mismatch (asked for string, got uint, etc) */ + MMDB_ERR_IPV6_IN_V4, /**< Tried to look up an IPv6 address in an IPv4-only db */ + MMDB_ERR_BADARG, /**< Invalid argument (e.g. unparseable IP address) */ +} MMDB_Status; + +/** Database metadata */ +typedef struct { + uint32_t node_count; /**< Number of nodes in the search tree */ + uint16_t record_size; /**< Size of each record in bits */ + uint16_t ip_version; /**< IP version the database covers (4 or 6) */ + uint64_t build_epoch; /**< Unix timestamp when the database was built */ + char database_type[128]; /**< Database type string (e.g. "GeoLite2-Country") */ +} MMDB_Metadata; + +/** Database handle */ +typedef struct { + uint8_t *data; /**< mmap'd (or malloc'd) file contents */ + size_t data_size; /**< Total file size */ + size_t data_section_offset; /**< Offset where data section starts */ + size_t data_section_size; /**< Size of data section */ + MMDB_Metadata metadata; /**< Parsed database metadata */ + uint32_t ipv4_start_node; /**< Cached start node for IPv4 lookups in IPv6 dbs */ + int ipv4_start_bit_depth; /**< Bit depth at ipv4_start_node */ + int is_mmap; /**< 1 if data was mmap'd, 0 if malloc'd */ +} MMDB_DB; + +/** Lookup result */ +typedef struct { + MMDB_DB *db; /**< Database this result belongs to */ + size_t offset; /**< Offset into data section, or 0 if not found */ + int has_data; /**< 1 if IP was found and has data */ +} MMDB_Result; + +/** Open an MMDB database file. + * Uses mmap where available, falls back to malloc+read. + * @param db Database handle to initialize + * @param filename Path to the .mmdb file + * @returns MMDB_OK on success, or an error code + */ +MMDB_Status mmdb_open(MMDB_DB *db, const char *filename); + +/** Close the database and release resources. + * Safe to call on an already-closed or zero-initialized handle. + * @param db Database handle to close + */ +void mmdb_close(MMDB_DB *db); + +/** Look up an IP address given as a string (IPv4 or IPv6). + * On success, check result->has_data to see if the IP was + * actually found in the database. + * @param db Database handle + * @param ip_str IP address string (e.g. "1.2.3.4" or "2001:db8::1") + * @param result Lookup result (output) + * @returns MMDB_OK on success, or an error code + */ +MMDB_Status mmdb_lookup(MMDB_DB *db, const char *ip_str, MMDB_Result *result); + +/** Look up an IP address given as a sockaddr. + * Supports sockaddr_in (IPv4) and sockaddr_in6 (IPv6). + * @param db Database handle + * @param sa Socket address to look up + * @param result Lookup result (output) + * @returns MMDB_OK on success, or an error code + */ +MMDB_Status mmdb_lookup_sockaddr(MMDB_DB *db, const struct sockaddr *sa, + MMDB_Result *result); + +/** Retrieve a string value from a lookup result by path. + * Returns a malloc'd, null-terminated copy. Caller must free(). + * On error, *out is set to NULL. + * @param result Lookup result from mmdb_lookup() + * @param out Receives a malloc'd null-terminated string (output) + * @param ... Path of map keys (NULL sentinel is added automatically) + * @returns MMDB_OK on success, or an error code + * @note Example: mmdb_get_str(&result, &val, "country", "iso_code"); + */ +MMDB_Status mmdb_do_get_str(MMDB_Result *result, char **out, ...); +#define mmdb_get_str(result, out, ...) mmdb_do_get_str(result, out, __VA_ARGS__, NULL) + +/** Retrieve a uint32 value from a lookup result by path. + * Also accepts uint16 values (promoted to uint32). + * On error, *out is set to 0. + * @param result Lookup result from mmdb_lookup() + * @param out Receives the uint32 value (output) + * @param ... Path of map keys (NULL sentinel is added automatically) + * @returns MMDB_OK on success, or an error code + * @note Example: mmdb_get_uint32(&result, &asn, "autonomous_system_number"); + */ +MMDB_Status mmdb_do_get_uint32(MMDB_Result *result, uint32_t *out, ...); +#define mmdb_get_uint32(result, out, ...) mmdb_do_get_uint32(result, out, __VA_ARGS__, NULL) + +/** Retrieve a boolean value from a lookup result by path. + * On error, *out is set to 0. + * @param result Lookup result from mmdb_lookup() + * @param out Receives the boolean value (0 or 1) (output) + * @param ... Path of map keys (NULL sentinel is added automatically) + * @returns MMDB_OK on success, or an error code + * @note Example: mmdb_get_bool(&result, &is_vpn, "is_anonymous_vpn"); + */ +MMDB_Status mmdb_do_get_bool(MMDB_Result *result, int *out, ...); +#define mmdb_get_bool(result, out, ...) mmdb_do_get_bool(result, out, __VA_ARGS__, NULL) + +/** Retrieve a double (float64) value from a lookup result by path. + * Also accepts float32 values (promoted to double). + * On error, *out is set to 0. + * @param result Lookup result from mmdb_lookup() + * @param out Receives the double value (output) + * @param ... Path of map keys (NULL sentinel is added automatically) + * @returns MMDB_OK on success, or an error code + * @note Example: mmdb_get_double(&result, &lat, "location", "latitude"); + */ +MMDB_Status mmdb_do_get_double(MMDB_Result *result, double *out, ...); +#define mmdb_get_double(result, out, ...) mmdb_do_get_double(result, out, __VA_ARGS__, NULL) + +/** Return a human-readable error string for a status code. + * @param err Status code to describe + * @returns Static string describing the error (never NULL) + */ +const char *mmdb_strerror(MMDB_Status err); + +#endif /* MMDB_H */ diff --git a/src/modules/Makefile.in b/src/modules/Makefile.in index 199c87b51..66c9be0b7 100644 --- a/src/modules/Makefile.in +++ b/src/modules/Makefile.in @@ -87,7 +87,8 @@ MODULES= \ central-api.so central-blocklist.so \ no-implicit-names.so maxperip.so utf8functions.so utf8only.so \ isupport.so extended-isupport.so \ - $(GEOIP_CLASSIC_OBJECTS) $(GEOIP_MAXMIND_OBJECTS) + $(GEOIP_CLASSIC_OBJECTS) \ + geoip_maxmind.so MODULEFLAGS=@MODULEFLAGS@ RM=@RM@ @@ -122,7 +123,7 @@ geoip_classic.so: geoip_classic.c $(INCLUDES) $(CC) $(CFLAGS) $(MODULEFLAGS) $(GEOIP_CLASSIC_CFLAGS) -DDYNAMIC_LINKING \ -o geoip_classic.so geoip_classic.c @LDFLAGS_PRIVATELIBS@ $(GEOIP_CLASSIC_LIBS) -# geoip_maxmind requires another extra library +# geoip_maxmind uses shipped mmdb.c geoip_maxmind.so: geoip_maxmind.c $(INCLUDES) - $(CC) $(CFLAGS) $(MODULEFLAGS) $(LIBMAXMINDDB_CFLAGS) -DDYNAMIC_LINKING \ - -o geoip_maxmind.so geoip_maxmind.c @LDFLAGS_PRIVATELIBS@ $(LIBMAXMINDDB_LIBS) + $(CC) $(CFLAGS) $(MODULEFLAGS) -DDYNAMIC_LINKING \ + -o geoip_maxmind.so geoip_maxmind.c mmdb.c diff --git a/src/modules/geoip_maxmind.c b/src/modules/geoip_maxmind.c index 20ba2aa8b..01d577038 100644 --- a/src/modules/geoip_maxmind.c +++ b/src/modules/geoip_maxmind.c @@ -4,12 +4,12 @@ */ #include "unrealircd.h" -#include +#include "mmdb.h" ModuleHeader MOD_HEADER = { "geoip_maxmind", - "5.1", + "5.2", "GEOIP using maxmind databases", "UnrealIRCd Team", "unrealircd-6", @@ -27,7 +27,7 @@ struct geoip_maxmind_config_s { /* Variables */ struct geoip_maxmind_config_s geoip_maxmind_config; -MMDB_s mmdb, asn_mmdb; +MMDB_DB mmdb, asn_mmdb; /* Forward declarations */ int geoip_maxmind_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs); @@ -40,8 +40,7 @@ int geoip_maxmind_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *err { ConfigEntry *cep; int errors = 0; - int i; - + if (type != CONFIG_SET) return 0; @@ -105,7 +104,7 @@ int geoip_maxmind_configposttest(int *errs) errors++; } if (!geoip_maxmind_config.have_asn_database) - safe_free(geoip_maxmind_config.db_file); /* at this point we aren't going to use ASN at all */ + safe_free(geoip_maxmind_config.asn_db_file); /* at this point we aren't going to use ASN at all */ } else { @@ -117,9 +116,8 @@ int geoip_maxmind_configposttest(int *errs) 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)); + config_warn("[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++; } if (is_file_readable(geoip_maxmind_config.asn_db_file, PERMDATADIR)) @@ -184,36 +182,36 @@ MOD_INIT() 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); + int status; - 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; + geoip_maxmind_free(); + + if (geoip_maxmind_config.db_file) + { + convert_to_absolute_path(&geoip_maxmind_config.db_file, PERMDATADIR); + status = mmdb_open(&mmdb, geoip_maxmind_config.db_file); + if (status != MMDB_OK) + { + unreal_log(ULOG_WARNING, "geoip_maxmind", "GEOIP_CANNOT_OPEN_DB", NULL, + "Could not open '$filename' - $mmdb_error", + log_data_string("filename", geoip_maxmind_config.db_file), + log_data_string("mmdb_error", mmdb_strerror(status))); + geoip_maxmind_config.have_database = 0; + } } - if (!geoip_maxmind_config.asn_db_file) /* if ASN file is unavailable, ignore it */ - return MOD_SUCCESS; - - convert_to_absolute_path(&geoip_maxmind_config.asn_db_file, PERMDATADIR); - - status = MMDB_open(geoip_maxmind_config.asn_db_file, MMDB_MODE_MMAP, &asn_mmdb); - - if (status != MMDB_SUCCESS) { - int save_err = errno; - unreal_log(ULOG_WARNING, "geoip_maxmind", "GEOIP_CANNOT_OPEN_ASN_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; + if (geoip_maxmind_config.asn_db_file) + { + convert_to_absolute_path(&geoip_maxmind_config.asn_db_file, PERMDATADIR); + status = mmdb_open(&asn_mmdb, geoip_maxmind_config.asn_db_file); + if (status != MMDB_OK) + { + unreal_log(ULOG_WARNING, "geoip_maxmind", "GEOIP_CANNOT_OPEN_ASN_DB", NULL, + "Could not open '$filename' - $mmdb_error", + log_data_string("filename", geoip_maxmind_config.asn_db_file), + log_data_string("mmdb_error", mmdb_strerror(status))); + geoip_maxmind_config.have_asn_database = 0; + } } return MOD_SUCCESS; @@ -229,17 +227,15 @@ MOD_UNLOAD() void geoip_maxmind_free(void) { - MMDB_close(&mmdb); - MMDB_close(&asn_mmdb); + mmdb_close(&mmdb); + mmdb_close(&asn_mmdb); } GeoIPResult *geoip_lookup_maxmind(char *ip) { - int gai_error, mmdb_error, status; - MMDB_lookup_result_s result; - MMDB_entry_data_s country_code, country_name, asn, asn_org; - char *country_code_str, *country_name_str, *asn_org_str; - GeoIPResult *r = NULL; + MMDB_Status status; + MMDB_Result result; + GeoIPResult *r; if (!ip) return NULL; @@ -248,91 +244,56 @@ GeoIPResult *geoip_lookup_maxmind(char *ip) return NULL; /* Country database */ - result = MMDB_lookup_string(&mmdb, ip, &gai_error, &mmdb_error); - if (gai_error) + status = mmdb_lookup(&mmdb, ip, &result); + if (status != MMDB_OK) { unreal_log(ULOG_DEBUG, "geoip_maxmind", "GEOIP_DB_ERROR", NULL, - "libmaxminddb: getaddrinfo error for $ip: $error", + "mmdb: lookup 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))); + log_data_string("error", mmdb_strerror(status))); return NULL; } - if (!result.found_entry) /* no result */ + if (!result.has_data) /* 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; - /* ASN database */ + if (mmdb_get_str(&result, &r->country_code, "country", "iso_code") != MMDB_OK) + { + free_geoip_result(r); + return NULL; + } + + if (mmdb_get_str(&result, &r->country_name, "country", "names", "en") != MMDB_OK) + { + free_geoip_result(r); + return NULL; + } + + /* No ASN database? Then we are done. */ if (!geoip_maxmind_config.have_asn_database) return r; - result = MMDB_lookup_string(&asn_mmdb, ip, &gai_error, &mmdb_error); + status = mmdb_lookup(&asn_mmdb, ip, &result); - if (gai_error) + if (status != MMDB_OK) { unreal_log(ULOG_DEBUG, "geoip_maxmind", "GEOIP_ASN_DB_ERROR", NULL, - "libmaxminddb: getaddrinfo error for $ip: $error", + "mmdb: lookup error for $ip: $error", log_data_string("ip", ip), - log_data_string("error", gai_strerror(gai_error))); + log_data_string("error", mmdb_strerror(status))); return r; } - if (mmdb_error != MMDB_SUCCESS) - { - unreal_log(ULOG_DEBUG, "geoip_maxmind", "GEOIP_ASN_DB_ERROR", NULL, - "libmaxminddb: library error for $ip: $error", - log_data_string("ip", ip), - log_data_string("error", MMDB_strerror(mmdb_error))); - return r; - } + if (!result.has_data) + return r; /* no ASN result, we are done. */ - if (!result.found_entry) /* no result */ + if (mmdb_get_uint32(&result, &r->asn, "autonomous_system_number") != MMDB_OK) return r; - status = MMDB_get_value(&result.entry, &asn, "autonomous_system_number", NULL); - if (status != MMDB_SUCCESS || !asn.has_data || asn.type != MMDB_DATA_TYPE_UINT32) + if (mmdb_get_str(&result, &r->asname, "autonomous_system_organization") != MMDB_OK) return r; - status = MMDB_get_value(&result.entry, &asn_org, "autonomous_system_organization", NULL); - if (status != MMDB_SUCCESS || !asn_org.has_data || asn_org.type != MMDB_DATA_TYPE_UTF8_STRING) - return r; - - if (!r) - r = safe_alloc(sizeof(GeoIPResult)); - - asn_org_str = safe_alloc(asn_org.data_size + 1); - memcpy(asn_org_str, asn_org.utf8_string, asn_org.data_size); - asn_org_str[asn_org.data_size] = '\0'; - - r->asn = asn.uint32; - r->asname = asn_org_str; return r; } - diff --git a/src/modules/mmdb.c b/src/modules/mmdb.c new file mode 100644 index 000000000..e322efe2a --- /dev/null +++ b/src/modules/mmdb.c @@ -0,0 +1,1211 @@ +/* + * mmdb.c - Minimal MMDB (MaxMind DB) reader library + * + * Written from the MaxMind DB file format specification + * (https://maxmind.github.io/MaxMind-DB/). + * + * This C implementation was written by the UnrealIRCd team, + * using the Go MMDB reader oschwald/maxminddb-golang by + * Gregory J. Oschwald as a reference during development. + * + * Copyright (c) 2015 Gregory J. Oschwald (Go implementation) + * Copyright (c) 2026 UnrealIRCd team + * + * Permission to use, copy, modify, and/or distribute this software for + * any purpose with or without fee is hereby granted, provided that + * the above copyright notice and this permission notice appear in + * all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL + * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE + * AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR + * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, + * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "mmdb.h" + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +/* Constants */ + +static const uint8_t METADATA_MARKER[] = "\xAB\xCD\xEF" "MaxMind.com"; +#define METADATA_MARKER_LEN 14 +#define METADATA_MAX_SIZE (128 * 1024) +#define DATA_SEPARATOR_SIZE 16 +#define MAX_DECODE_DEPTH 64 + +/* MMDB data types from the spec */ +enum { + DT_EXTENDED = 0, + DT_POINTER = 1, + DT_STRING = 2, + DT_FLOAT64 = 3, + DT_BYTES = 4, + DT_UINT16 = 5, + DT_UINT32 = 6, + DT_MAP = 7, + DT_INT32 = 8, + DT_UINT64 = 9, + DT_UINT128 = 10, + DT_ARRAY = 11, + DT_BOOL = 14, + DT_FLOAT32 = 15, +}; + +/* Platform: mmap / file I/O */ + +#ifdef _WIN32 + +static uint8_t *mmdb_mmap_file(const char *filename, size_t *size_out, int *is_mmap) +{ + HANDLE hFile, hMap; + LARGE_INTEGER fileSize; + uint8_t *data; + + hFile = CreateFileA(filename, GENERIC_READ, FILE_SHARE_READ, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (hFile == INVALID_HANDLE_VALUE) + return NULL; + + if (!GetFileSizeEx(hFile, &fileSize) || fileSize.QuadPart == 0 || + (uint64_t)fileSize.QuadPart > SIZE_MAX) + { + CloseHandle(hFile); + return NULL; + } + + hMap = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, NULL); + CloseHandle(hFile); + if (!hMap) + return NULL; + + data = (uint8_t *)MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0); + CloseHandle(hMap); + if (!data) + return NULL; + + *size_out = (size_t)fileSize.QuadPart; + *is_mmap = 1; + return data; +} + +static void mmdb_munmap(uint8_t *data, size_t size) +{ + (void)size; + UnmapViewOfFile(data); +} + +#else /* Unix */ + +static uint8_t *mmdb_mmap_file(const char *filename, size_t *size_out, int *is_mmap) +{ + int fd; + struct stat st; + uint8_t *data; + + fd = open(filename, O_RDONLY); + if (fd < 0) + return NULL; + + if (fstat(fd, &st) < 0 || st.st_size <= 0) + { + close(fd); + return NULL; + } + + data = (uint8_t *)mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + if (data == MAP_FAILED) + return NULL; + + *size_out = (size_t)st.st_size; + *is_mmap = 1; + return data; +} + +static void mmdb_munmap(uint8_t *data, size_t size) +{ + munmap(data, size); +} + +#endif + +/* Data section decoder + * + * These functions decode values from the MMDB data section. + * The "buffer" and "buflen" refer to the data section (or + * metadata section) only, not the whole file. + */ + +/* Decode size from control byte per the spec's payload size rules. */ +static int decode_size(const uint8_t *buf, size_t buflen, + uint32_t raw_size, size_t offset, + uint32_t *size_out, size_t *new_offset) +{ + if (raw_size < 29) + { + *size_out = raw_size; + *new_offset = offset; + return MMDB_OK; + } + if (raw_size == 29) + { + if (offset >= buflen) + return MMDB_ERR_CORRUPT; + *size_out = 29 + (uint32_t)buf[offset]; + *new_offset = offset + 1; + return MMDB_OK; + } + if (raw_size == 30) + { + if (offset + 2 > buflen) + return MMDB_ERR_CORRUPT; + *size_out = 285 + ((uint32_t)buf[offset] << 8) + (uint32_t)buf[offset + 1]; + *new_offset = offset + 2; + return MMDB_OK; + } + /* raw_size == 31 */ + if (offset + 3 > buflen) + return MMDB_ERR_CORRUPT; + *size_out = 65821 + + ((uint32_t)buf[offset] << 16) + + ((uint32_t)buf[offset + 1] << 8) + + (uint32_t)buf[offset + 2]; + *new_offset = offset + 3; + return MMDB_OK; +} + +/* Decode a control byte: returns data type, payload size, and new offset. */ +static int decode_ctrl(const uint8_t *buf, size_t buflen, size_t offset, + int *type_out, uint32_t *size_out, size_t *data_offset) +{ + int type; + uint32_t raw_size; + size_t off; + + if (offset >= buflen) + return MMDB_ERR_CORRUPT; + + type = (buf[offset] >> 5) & 0x7; + raw_size = buf[offset] & 0x1F; + off = offset + 1; + + if (type == DT_EXTENDED) + { + if (off >= buflen) + return MMDB_ERR_CORRUPT; + type = (int)buf[off] + 7; + off++; + } + + if (type == DT_POINTER) + { + /* For pointers, the size field encodes the pointer value, not a payload size. + * We return raw_size in size_out for the caller to decode. */ + *type_out = DT_POINTER; + *size_out = raw_size; + *data_offset = off; + return MMDB_OK; + } + + *type_out = type; + return decode_size(buf, buflen, raw_size, off, size_out, data_offset); +} + +/* Decode a pointer value per the spec. Returns the offset it points to. */ +static int decode_pointer(const uint8_t *buf, size_t buflen, + uint32_t ctrl_size, size_t offset, + size_t *pointer_out, size_t *new_offset) +{ + uint32_t ptr_size = ((ctrl_size >> 3) & 0x3) + 1; + size_t end = offset + ptr_size; + size_t pointer; + uint32_t prefix; + size_t i; + + if (end > buflen) + return MMDB_ERR_CORRUPT; + + prefix = (ptr_size == 4) ? 0 : (ctrl_size & 0x7); + + /* Build pointer from prefix + pointer bytes (big-endian) */ + pointer = prefix; + for (i = offset; i < end; i++) + pointer = (pointer << 8) | (size_t)buf[i]; + + /* Add base offset per pointer size */ + switch (ptr_size) + { + case 1: break; + case 2: pointer += 2048; break; + case 3: pointer += 526336; break; + case 4: break; + } + + *pointer_out = pointer; + *new_offset = end; + return MMDB_OK; +} + +/* Read a uint32 from variable-length big-endian bytes in the data section. */ +static int decode_uint32(const uint8_t *buf, size_t buflen, + uint32_t size, size_t offset, uint32_t *out) +{ + uint32_t i; + + if (size > 4 || offset + size > buflen) + return MMDB_ERR_CORRUPT; + *out = 0; + for (i = 0; i < size; i++) + *out = (*out << 8) | (uint32_t)buf[offset + i]; + return MMDB_OK; +} + +/* Read a uint64 from variable-length big-endian bytes. */ +static int decode_uint64(const uint8_t *buf, size_t buflen, + uint32_t size, size_t offset, uint64_t *out) +{ + uint32_t i; + + if (size > 8 || offset + size > buflen) + return MMDB_ERR_CORRUPT; + *out = 0; + for (i = 0; i < size; i++) + *out = (*out << 8) | (uint64_t)buf[offset + i]; + return MMDB_OK; +} + +/* Read a uint16 from variable-length big-endian bytes. */ +static int decode_uint16(const uint8_t *buf, size_t buflen, + uint32_t size, size_t offset, uint16_t *out) +{ + uint32_t i; + + if (size > 2 || offset + size > buflen) + return MMDB_ERR_CORRUPT; + *out = 0; + for (i = 0; i < size; i++) + *out = (uint16_t)((*out << 8) | (uint16_t)buf[offset + i]); + return MMDB_OK; +} + +/* Read a float64 (double) stored as IEEE-754 big-endian. */ +static int decode_float64(const uint8_t *buf, size_t buflen, + uint32_t size, size_t offset, double *out) +{ + union { uint64_t u; double d; } conv; + int i; + + if (size != 8 || offset + 8 > buflen) + return MMDB_ERR_CORRUPT; + conv.u = 0; + for (i = 0; i < 8; i++) + conv.u = (conv.u << 8) | (uint64_t)buf[offset + i]; + *out = conv.d; + return MMDB_OK; +} + +/* Read a float32 stored as IEEE-754 big-endian. */ +static int decode_float32(const uint8_t *buf, size_t buflen, + uint32_t size, size_t offset, float *out) +{ + union { uint32_t u; float f; } conv; + int i; + + if (size != 4 || offset + 4 > buflen) + return MMDB_ERR_CORRUPT; + conv.u = 0; + for (i = 0; i < 4; i++) + conv.u = (conv.u << 8) | (uint32_t)buf[offset + i]; + *out = conv.f; + return MMDB_OK; +} + +/* Skip over a data field without decoding it. Used to skip values + * in maps when searching for a specific key. */ +static int skip_value(const uint8_t *buf, size_t buflen, size_t offset, + size_t *new_offset, int depth) +{ + int type; + uint32_t size; + size_t off; + int err; + uint32_t i; + uint32_t ptr_size; + + if (depth > MAX_DECODE_DEPTH) + return MMDB_ERR_CORRUPT; + + err = decode_ctrl(buf, buflen, offset, &type, &size, &off); + if (err) + return err; + + if (type == DT_POINTER) + { + /* Pointer is self-contained; just skip past the pointer bytes */ + ptr_size = ((size >> 3) & 0x3) + 1; + if (off + ptr_size > buflen) + return MMDB_ERR_CORRUPT; + *new_offset = off + ptr_size; + return MMDB_OK; + } + + if (type == DT_MAP) + { + /* Skip 2*size entries (key + value for each pair) */ + for (i = 0; i < size * 2; i++) + { + err = skip_value(buf, buflen, off, &off, depth + 1); + if (err) + return err; + } + *new_offset = off; + return MMDB_OK; + } + + if (type == DT_ARRAY) + { + for (i = 0; i < size; i++) + { + err = skip_value(buf, buflen, off, &off, depth + 1); + if (err) + return err; + } + *new_offset = off; + return MMDB_OK; + } + + if (type == DT_BOOL) + { + /* Bool has no payload; size encodes the value */ + *new_offset = off; + return MMDB_OK; + } + + /* Scalar types: payload is 'size' bytes */ + if (off + size > buflen) + return MMDB_ERR_CORRUPT; + *new_offset = off + size; + return MMDB_OK; +} + +/* Resolve a data entry at a given offset, following pointers + * if needed. Returns the type, size, and offset of the actual + * data payload. */ +static int resolve_entry(const uint8_t *buf, size_t buflen, size_t offset, + int *type_out, uint32_t *size_out, size_t *data_offset) +{ + int type; + uint32_t size; + size_t off; + int err; + size_t pointer; + + err = decode_ctrl(buf, buflen, offset, &type, &size, &off); + if (err) + return err; + + if (type == DT_POINTER) + { + err = decode_pointer(buf, buflen, size, off, &pointer, &off); + if (err) + return err; + /* Follow the pointer (only one level allowed per spec) */ + err = decode_ctrl(buf, buflen, pointer, &type, &size, &off); + if (err) + return err; + if (type == DT_POINTER) + return MMDB_ERR_CORRUPT; /* pointer to pointer is illegal */ + } + + *type_out = type; + *size_out = size; + *data_offset = off; + return MMDB_OK; +} + +/* Find a key in a map. + * + * Given the data section buffer and an offset pointing to a map, + * find the entry with the given key. Returns the offset of the + * value's control byte. */ +static int map_find_key(const uint8_t *buf, size_t buflen, + size_t map_offset, const char *key, + size_t *value_offset) +{ + int type; + uint32_t map_size; + size_t off; + int err; + size_t key_len = strlen(key); + uint32_t i; + int ktype; + uint32_t ksize; + size_t koff; + size_t next_off; + + /* Decode the map entry at map_offset, following pointers */ + err = resolve_entry(buf, buflen, map_offset, &type, &map_size, &off); + if (err) + return err; + if (type != DT_MAP) + return MMDB_ERR_TYPE; + + for (i = 0; i < map_size; i++) + { + /* Decode key - may be a pointer */ + err = resolve_entry(buf, buflen, off, &ktype, &ksize, &koff); + if (err) + return err; + if (ktype != DT_STRING) + return MMDB_ERR_CORRUPT; + + /* We need to advance past the key in the stream. + * The key's position in the stream is at 'off', so skip it. */ + err = skip_value(buf, buflen, off, &next_off, 0); + if (err) + return err; + + /* Compare key */ + if (koff + ksize <= buflen && + ksize == (uint32_t)key_len && + memcmp(buf + koff, key, key_len) == 0) + { + *value_offset = next_off; + return MMDB_OK; + } + + /* Skip the value */ + err = skip_value(buf, buflen, next_off, &off, 0); + if (err) + return err; + } + + return MMDB_ERR_NODATA; +} + +/* Walk a path of keys through nested maps */ +static int walk_path(const uint8_t *buf, size_t buflen, + size_t start_offset, va_list ap, + size_t *final_offset) +{ + size_t offset = start_offset; + const char *key; + int err; + + while ((key = va_arg(ap, const char *)) != NULL) + { + err = map_find_key(buf, buflen, offset, key, &offset); + if (err) + return err; + } + + *final_offset = offset; + return MMDB_OK; +} + +/* Metadata parsing */ + +/* Find the metadata marker by searching backwards from the end of file. */ +static const uint8_t *find_metadata(const uint8_t *data, size_t data_size) +{ + size_t scan_size; + const uint8_t *p; + + if (data_size < METADATA_MARKER_LEN) + return NULL; + + scan_size = data_size; + if (scan_size > METADATA_MAX_SIZE) + scan_size = METADATA_MAX_SIZE; + + /* Search backwards for the last occurrence */ + p = data + data_size - METADATA_MARKER_LEN; + while (p >= data + data_size - scan_size) + { + if (memcmp(p, METADATA_MARKER, METADATA_MARKER_LEN) == 0) + return p; + if (p == data) + break; + p--; + } + return NULL; +} + +/* Parse metadata from the metadata section into db->metadata. + * The metadata is itself an MMDB data section containing a map. */ +static int parse_metadata(MMDB_DB *db, const uint8_t *meta_buf, size_t meta_len) +{ + int type; + uint32_t map_size; + size_t off; + int err; + uint32_t i; + int ktype; + uint32_t ksize; + size_t koff; + size_t val_off; + int vtype; + uint32_t vsize; + size_t voff; + uint32_t val_u32; + uint16_t val_u16; + uint64_t val_u64; + size_t copy_len; + + err = decode_ctrl(meta_buf, meta_len, 0, &type, &map_size, &off); + if (err) + return MMDB_ERR_INVALID_DB; + if (type != DT_MAP) + return MMDB_ERR_INVALID_DB; + + memset(&db->metadata, 0, sizeof(db->metadata)); + + for (i = 0; i < map_size; i++) + { + /* Decode key */ + err = resolve_entry(meta_buf, meta_len, off, &ktype, &ksize, &koff); + if (err) + return MMDB_ERR_INVALID_DB; + if (ktype != DT_STRING) + return MMDB_ERR_INVALID_DB; + if (koff + ksize > meta_len) + return MMDB_ERR_INVALID_DB; + + /* Skip key in stream */ + err = skip_value(meta_buf, meta_len, off, &val_off, 0); + if (err) + return MMDB_ERR_INVALID_DB; + + /* Decode value depending on which key this is */ + err = resolve_entry(meta_buf, meta_len, val_off, &vtype, &vsize, &voff); + if (err) + return MMDB_ERR_INVALID_DB; + + if (ksize == 10 && memcmp(meta_buf + koff, "node_count", 10) == 0) + { + if (vtype != DT_UINT32 && vtype != DT_UINT16) + return MMDB_ERR_INVALID_DB; + err = decode_uint32(meta_buf, meta_len, vsize, voff, &val_u32); + if (err) + return MMDB_ERR_INVALID_DB; + db->metadata.node_count = val_u32; + } else if (ksize == 11 && memcmp(meta_buf + koff, "record_size", 11) == 0) + { + if (vtype != DT_UINT16 && vtype != DT_UINT32) + return MMDB_ERR_INVALID_DB; + err = decode_uint16(meta_buf, meta_len, vsize, voff, &val_u16); + if (err) + return MMDB_ERR_INVALID_DB; + db->metadata.record_size = val_u16; + } else if (ksize == 10 && memcmp(meta_buf + koff, "ip_version", 10) == 0) + { + if (vtype != DT_UINT16 && vtype != DT_UINT32) + return MMDB_ERR_INVALID_DB; + err = decode_uint16(meta_buf, meta_len, vsize, voff, &val_u16); + if (err) + return MMDB_ERR_INVALID_DB; + db->metadata.ip_version = val_u16; + } else if (ksize == 11 && memcmp(meta_buf + koff, "build_epoch", 11) == 0) + { + if (vtype != DT_UINT64 && vtype != DT_UINT32) + return MMDB_ERR_INVALID_DB; + err = decode_uint64(meta_buf, meta_len, vsize, voff, &val_u64); + if (err) + return MMDB_ERR_INVALID_DB; + db->metadata.build_epoch = val_u64; + } else if (ksize == 13 && memcmp(meta_buf + koff, "database_type", 13) == 0) + { + if (vtype != DT_STRING) + return MMDB_ERR_INVALID_DB; + copy_len = vsize; + if (copy_len >= sizeof(db->metadata.database_type)) + copy_len = sizeof(db->metadata.database_type) - 1; + if (voff + copy_len > meta_len) + return MMDB_ERR_INVALID_DB; + memcpy(db->metadata.database_type, meta_buf + voff, copy_len); + db->metadata.database_type[copy_len] = '\0'; + } + + /* Skip the value in the stream to advance to the next key */ + err = skip_value(meta_buf, meta_len, val_off, &off, 0); + if (err) + return MMDB_ERR_INVALID_DB; + } + + /* Validate required fields */ + if (db->metadata.node_count == 0 || + db->metadata.record_size == 0 || + (db->metadata.ip_version != 4 && db->metadata.ip_version != 6)) + { + return MMDB_ERR_INVALID_DB; + } + + /* Only 24, 28, 32 bit records are supported */ + if (db->metadata.record_size != 24 && + db->metadata.record_size != 28 && + db->metadata.record_size != 32) + { + return MMDB_ERR_INVALID_DB; + } + + return MMDB_OK; +} + +/* Search tree traversal */ + +/* Read a single record from a node. bit=0 for left, bit=1 for right. */ +static int read_node(const uint8_t *buf, size_t buflen, + uint32_t node, uint32_t bit, uint32_t record_size, + uint32_t node_offset_mult, uint32_t *value) +{ + size_t offset = (size_t)node * node_offset_mult; + size_t o; + + switch (record_size) + { + case 24: + { + o = offset + bit * 3; + if (o + 3 > buflen) + return MMDB_ERR_CORRUPT; + *value = ((uint32_t)buf[o] << 16) | + ((uint32_t)buf[o + 1] << 8) | + (uint32_t)buf[o + 2]; + return MMDB_OK; + } + case 28: + { + if (offset + 7 > buflen) + return MMDB_ERR_CORRUPT; + if (bit == 0) + { + *value = (((uint32_t)buf[offset + 3] & 0xF0) << 20) | + ((uint32_t)buf[offset] << 16) | + ((uint32_t)buf[offset + 1] << 8) | + (uint32_t)buf[offset + 2]; + } else + { + *value = (((uint32_t)buf[offset + 3] & 0x0F) << 24) | + ((uint32_t)buf[offset + 4] << 16) | + ((uint32_t)buf[offset + 5] << 8) | + (uint32_t)buf[offset + 6]; + } + return MMDB_OK; + } + case 32: + { + o = offset + bit * 4; + if (o + 4 > buflen) + return MMDB_ERR_CORRUPT; + *value = ((uint32_t)buf[o] << 24) | + ((uint32_t)buf[o + 1] << 16) | + ((uint32_t)buf[o + 2] << 8) | + (uint32_t)buf[o + 3]; + return MMDB_OK; + } + } + return MMDB_ERR_CORRUPT; +} + +/* Traverse the search tree for a 128-bit IP (IPv6 or IPv4-mapped). */ +static int traverse_tree(MMDB_DB *db, const uint8_t ip[16], + int start_bit, uint32_t start_node, + uint32_t *result_node, int *prefix_len) +{ + uint32_t node = start_node; + uint32_t node_count = db->metadata.node_count; + uint32_t record_size = db->metadata.record_size; + uint32_t node_offset_mult = record_size / 4; + uint32_t bit; + int i; + int err; + + for (i = start_bit; i < 128 && node < node_count; i++) + { + bit = (ip[i >> 3] >> (7 - (i & 7))) & 1; + err = read_node(db->data, db->data_size, node, bit, + record_size, node_offset_mult, &node); + if (err) + return err; + } + + *result_node = node; + *prefix_len = i; + return MMDB_OK; +} + +/* Pre-walk the first 96 zero bits to find the IPv4 subtree start + * in an IPv6 database. */ +static int find_ipv4_start(MMDB_DB *db) +{ + uint32_t node = 0; + uint32_t node_count = db->metadata.node_count; + uint32_t record_size = db->metadata.record_size; + uint32_t node_offset_mult = record_size / 4; + int i; + int err; + + db->ipv4_start_bit_depth = 96; + + for (i = 0; i < 96 && node < node_count; i++) + { + err = read_node(db->data, db->data_size, node, 0, + record_size, node_offset_mult, &node); + if (err) + return err; + } + + db->ipv4_start_node = node; + db->ipv4_start_bit_depth = i; + return MMDB_OK; +} + +/* Lookup core */ + +static int lookup_ip128(MMDB_DB *db, const uint8_t ip[16], int is_ipv4, + MMDB_Result *result) +{ + uint32_t node; + int prefix_len; + int err; + int start_bit; + uint32_t start_node; + size_t data_offset; + + result->db = db; + result->offset = 0; + result->has_data = 0; + + if (is_ipv4) + { + start_bit = db->ipv4_start_bit_depth; + start_node = db->ipv4_start_node; + } else + { + start_bit = 0; + start_node = 0; + } + + err = traverse_tree(db, ip, start_bit, start_node, &node, &prefix_len); + if (err) + return err; + + if (node == db->metadata.node_count) + { + /* No data for this IP */ + return MMDB_OK; + } + if (node > db->metadata.node_count) + { + /* Pointer into data section */ + data_offset = (size_t)(node - db->metadata.node_count) - DATA_SEPARATOR_SIZE; + if (data_offset >= db->data_section_size) + return MMDB_ERR_CORRUPT; + result->offset = data_offset; + result->has_data = 1; + return MMDB_OK; + } + + return MMDB_ERR_CORRUPT; +} + +/* Public API */ + +MMDB_Status mmdb_open(MMDB_DB *db, const char *filename) +{ + const uint8_t *meta_start; + size_t meta_offset; + size_t search_tree_size; + size_t meta_marker_offset; + int err; + + if (!db || !filename) + return MMDB_ERR_BADARG; + + memset(db, 0, sizeof(*db)); + + db->data = mmdb_mmap_file(filename, &db->data_size, &db->is_mmap); + if (!db->data) + return MMDB_ERR_OPEN; + + /* Find metadata marker */ + meta_start = find_metadata(db->data, db->data_size); + if (!meta_start) + { + mmdb_close(db); + return MMDB_ERR_INVALID_DB; + } + + meta_offset = (size_t)(meta_start - db->data) + METADATA_MARKER_LEN; + + /* Parse metadata */ + err = parse_metadata(db, db->data + meta_offset, + db->data_size - meta_offset); + if (err) + { + mmdb_close(db); + return err; + } + + /* Calculate section offsets. Per the spec: + * search_tree_size = (record_size * 2 / 8) * node_count + * = (record_size / 4) * node_count + */ + if (db->metadata.node_count > SIZE_MAX / (db->metadata.record_size / 4)) + { + mmdb_close(db); + return MMDB_ERR_INVALID_DB; + } + search_tree_size = (size_t)(db->metadata.record_size / 4) * + (size_t)db->metadata.node_count; + db->data_section_offset = search_tree_size + DATA_SEPARATOR_SIZE; + + /* data section ends where the metadata marker begins */ + meta_marker_offset = (size_t)(meta_start - db->data); + if (db->data_section_offset > meta_marker_offset) + { + mmdb_close(db); + return MMDB_ERR_INVALID_DB; + } + db->data_section_size = meta_marker_offset - db->data_section_offset; + + /* For IPv6 databases, find the IPv4 subtree start */ + if (db->metadata.ip_version == 6) + { + err = find_ipv4_start(db); + if (err) + { + mmdb_close(db); + return err; + } + } else + { + db->ipv4_start_node = 0; + db->ipv4_start_bit_depth = 96; + } + + return MMDB_OK; +} + +void mmdb_close(MMDB_DB *db) +{ + if (!db) + return; + if (db->data) + { + if (db->is_mmap) + mmdb_munmap(db->data, db->data_size); + else + free(db->data); + db->data = NULL; + } + db->data_size = 0; +} + +MMDB_Status mmdb_lookup(MMDB_DB *db, const char *ip_str, MMDB_Result *result) +{ + uint8_t ip128[16]; + struct in_addr addr4; + struct in6_addr addr6; + + if (!db || !db->data || !ip_str || !result) + return MMDB_ERR_BADARG; + + memset(ip128, 0, sizeof(ip128)); + + if (inet_pton(AF_INET, ip_str, &addr4) == 1) + { + /* MMDB always uses a 128-bit search buffer. + * IPv4 goes in the last 4 bytes (offset 12). + * The is_ipv4 flag skips the first 96 bits + * so traversal begins at the IPv4 address. + */ + memcpy(ip128 + 12, &addr4.s_addr, 4); + return lookup_ip128(db, ip128, 1, result); + } + + if (inet_pton(AF_INET6, ip_str, &addr6) == 1) + { + if (db->metadata.ip_version == 4) + return MMDB_ERR_IPV6_IN_V4; + memcpy(ip128, addr6.s6_addr, 16); + return lookup_ip128(db, ip128, 0, result); + } + + return MMDB_ERR_BADARG; +} + +MMDB_Status mmdb_lookup_sockaddr(MMDB_DB *db, const struct sockaddr *sa, + MMDB_Result *result) +{ + uint8_t ip128[16]; + const struct sockaddr_in *sa4; + const struct sockaddr_in6 *sa6; + + if (!db || !db->data || !sa || !result) + return MMDB_ERR_BADARG; + + memset(ip128, 0, sizeof(ip128)); + + if (sa->sa_family == AF_INET) + { + sa4 = (const struct sockaddr_in *)sa; + memcpy(ip128 + 12, &sa4->sin_addr.s_addr, 4); + return lookup_ip128(db, ip128, 1, result); + } + + if (sa->sa_family == AF_INET6) + { + sa6 = (const struct sockaddr_in6 *)sa; + if (db->metadata.ip_version == 4) + return MMDB_ERR_IPV6_IN_V4; + memcpy(ip128, sa6->sin6_addr.s6_addr, 16); + return lookup_ip128(db, ip128, 0, result); + } + + return MMDB_ERR_BADARG; +} + +static MMDB_Status mmdb_get_str_raw_va(MMDB_Result *result, const char **out, size_t *len, va_list ap) +{ + size_t offset; + int err; + int type; + uint32_t size; + size_t data_off; + const uint8_t *dsec; + size_t dsec_len; + + if (!result || !result->db || !out || !len) + return MMDB_ERR_BADARG; + + *out = NULL; + *len = 0; + + if (!result->has_data) + return MMDB_ERR_NODATA; + + dsec = result->db->data + result->db->data_section_offset; + dsec_len = result->db->data_section_size; + + /* Walk the path */ + err = walk_path(dsec, dsec_len, result->offset, ap, &offset); + if (err) + return err; + + /* Resolve the final value */ + err = resolve_entry(dsec, dsec_len, offset, &type, &size, &data_off); + if (err) + return err; + if (type != DT_STRING) + return MMDB_ERR_TYPE; + if (data_off + size > dsec_len) + return MMDB_ERR_CORRUPT; + + *out = (const char *)(dsec + data_off); + *len = (size_t)size; + return MMDB_OK; +} + +MMDB_Status mmdb_do_get_str(MMDB_Result *result, char **out, ...) +{ + const char *ptr; + size_t len; + va_list ap; + int err; + + if (!out) + return MMDB_ERR_BADARG; + + *out = NULL; + va_start(ap, out); + err = mmdb_get_str_raw_va(result, &ptr, &len, ap); + va_end(ap); + if (err) + return err; + + *out = malloc(len + 1); + if (!*out) + return MMDB_ERR_OPEN; + memcpy(*out, ptr, len); + (*out)[len] = '\0'; + return MMDB_OK; +} + +MMDB_Status mmdb_do_get_uint32(MMDB_Result *result, uint32_t *out, ...) +{ + va_list ap; + size_t offset; + int err; + int type; + uint32_t size; + size_t data_off; + const uint8_t *dsec; + size_t dsec_len; + + if (!result || !result->db || !out) + return MMDB_ERR_BADARG; + + *out = 0; + + if (!result->has_data) + return MMDB_ERR_NODATA; + + dsec = result->db->data + result->db->data_section_offset; + dsec_len = result->db->data_section_size; + + /* Walk the path */ + va_start(ap, out); + err = walk_path(dsec, dsec_len, result->offset, ap, &offset); + va_end(ap); + if (err) + return err; + + /* Resolve the final value */ + err = resolve_entry(dsec, dsec_len, offset, &type, &size, &data_off); + if (err) + return err; + + /* Accept both uint16 and uint32 */ + if (type == DT_UINT32 || type == DT_UINT16) + { + return decode_uint32(dsec, dsec_len, size, data_off, out); + } + + return MMDB_ERR_TYPE; +} + +MMDB_Status mmdb_do_get_bool(MMDB_Result *result, int *out, ...) +{ + va_list ap; + size_t offset; + int err; + int type; + uint32_t size; + size_t data_off; + const uint8_t *dsec; + size_t dsec_len; + + if (!result || !result->db || !out) + return MMDB_ERR_BADARG; + + *out = 0; + + if (!result->has_data) + return MMDB_ERR_NODATA; + + dsec = result->db->data + result->db->data_section_offset; + dsec_len = result->db->data_section_size; + + va_start(ap, out); + err = walk_path(dsec, dsec_len, result->offset, ap, &offset); + va_end(ap); + if (err) + return err; + + err = resolve_entry(dsec, dsec_len, offset, &type, &size, &data_off); + if (err) + return err; + + if (type != DT_BOOL) + return MMDB_ERR_TYPE; + + /* Per spec, boolean size is 0 (false) or 1 (true), no payload */ + *out = (size != 0) ? 1 : 0; + return MMDB_OK; +} + +MMDB_Status mmdb_do_get_double(MMDB_Result *result, double *out, ...) +{ + va_list ap; + size_t offset; + int err; + int type; + uint32_t size; + size_t data_off; + const uint8_t *dsec; + size_t dsec_len; + float f; + + if (!result || !result->db || !out) + return MMDB_ERR_BADARG; + + *out = 0; + + if (!result->has_data) + return MMDB_ERR_NODATA; + + dsec = result->db->data + result->db->data_section_offset; + dsec_len = result->db->data_section_size; + + va_start(ap, out); + err = walk_path(dsec, dsec_len, result->offset, ap, &offset); + va_end(ap); + if (err) + return err; + + err = resolve_entry(dsec, dsec_len, offset, &type, &size, &data_off); + if (err) + return err; + + if (type == DT_FLOAT64) + { + return decode_float64(dsec, dsec_len, size, data_off, out); + } + if (type == DT_FLOAT32) + { + /* Promote float32 to double */ + err = decode_float32(dsec, dsec_len, size, data_off, &f); + if (err) + return err; + *out = (double)f; + return MMDB_OK; + } + + return MMDB_ERR_TYPE; +} + +const char *mmdb_strerror(MMDB_Status err) +{ + switch (err) + { + case MMDB_OK: + return "Success"; + case MMDB_ERR_OPEN: + return "Could not open database file"; + case MMDB_ERR_INVALID_DB: + return "Invalid MMDB database"; + case MMDB_ERR_BADARG: + return "Invalid argument"; + case MMDB_ERR_CORRUPT: + return "Corrupt database (search tree or data section)"; + case MMDB_ERR_NODATA: + return "No data found for the requested path"; + case MMDB_ERR_TYPE: + return "Data type mismatch"; + case MMDB_ERR_IPV6_IN_V4: + return "Cannot look up IPv6 address in IPv4-only database"; + default: + return "Unknown error"; + } +}