This is an automated email from the ASF dual-hosted git repository.

bcall pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new 8548ca967c Fix header_rewrite MaxMind geo lookups for GeoIP2/GeoLite2 
mmdb databases (#12948)
8548ca967c is described below

commit 8548ca967c8338682f33a262fedbb1aa9b45279f
Author: Bryan Call <[email protected]>
AuthorDate: Mon Mar 16 08:54:38 2026 -0700

    Fix header_rewrite MaxMind geo lookups for GeoIP2/GeoLite2 mmdb databases 
(#12948)
    
    * Fix MMDB field paths: use nested "country","iso_code" instead of
      nonexistent flat "country_code", matching old GeoIP backend behavior
    * Auto-detect MMDB schema at load time to support both vendor (flat)
      and standard (nested) database layouts transparently
    * Fix null-after-delete dangling pointer in initLibrary
    * Add MMDB path validation unit test and test database generation
    
    Fixes: #11812
---
 plugins/header_rewrite/CMakeLists.txt            |  36 +++++
 plugins/header_rewrite/conditions_geo_maxmind.cc | 102 +++++++-----
 plugins/header_rewrite/generate_test_mmdb.py     | 103 ++++++++++++
 plugins/header_rewrite/header_rewrite_test.cc    | 196 +++++++++++++++++++++++
 4 files changed, 393 insertions(+), 44 deletions(-)

diff --git a/plugins/header_rewrite/CMakeLists.txt 
b/plugins/header_rewrite/CMakeLists.txt
index 4c658b78ba..90ff5156c7 100644
--- a/plugins/header_rewrite/CMakeLists.txt
+++ b/plugins/header_rewrite/CMakeLists.txt
@@ -60,7 +60,43 @@ if(BUILD_TESTING)
   target_link_libraries(test_header_rewrite PRIVATE header_rewrite_parser 
ts::inkevent ts::tscore)
 
   if(maxminddb_FOUND)
+    target_compile_definitions(test_header_rewrite PRIVATE 
TS_USE_HRW_MAXMINDDB=1)
     target_link_libraries(test_header_rewrite PRIVATE maxminddb::maxminddb)
+
+    find_package(
+      Python3
+      COMPONENTS Interpreter
+      QUIET
+    )
+    if(Python3_FOUND)
+      execute_process(
+        COMMAND "${Python3_EXECUTABLE}" -c "import mmdb_writer; import netaddr"
+        RESULT_VARIABLE _mmdb_python_result
+        OUTPUT_QUIET ERROR_QUIET
+      )
+      if(_mmdb_python_result EQUAL 0)
+        set(_mmdb_test_dir "${CMAKE_CURRENT_BINARY_DIR}/test_mmdb")
+        add_custom_command(
+          OUTPUT "${_mmdb_test_dir}/test_flat_geo.mmdb" 
"${_mmdb_test_dir}/test_nested_geo.mmdb"
+          COMMAND ${CMAKE_COMMAND} -E make_directory "${_mmdb_test_dir}"
+          COMMAND ${Python3_EXECUTABLE} 
"${CMAKE_CURRENT_SOURCE_DIR}/generate_test_mmdb.py" "${_mmdb_test_dir}"
+          DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/generate_test_mmdb.py"
+          COMMENT "Generating test MMDB files for header_rewrite"
+        )
+        add_custom_target(
+          generate_test_mmdb DEPENDS "${_mmdb_test_dir}/test_flat_geo.mmdb" 
"${_mmdb_test_dir}/test_nested_geo.mmdb"
+        )
+        add_dependencies(test_header_rewrite generate_test_mmdb)
+        set_tests_properties(
+          test_header_rewrite
+          PROPERTIES
+            ENVIRONMENT
+            
"MMDB_TEST_FLAT=${_mmdb_test_dir}/test_flat_geo.mmdb;MMDB_TEST_NESTED=${_mmdb_test_dir}/test_nested_geo.mmdb"
+        )
+      else()
+        message(STATUS "Python modules 'mmdb-writer'/'netaddr' not found; 
skipping test MMDB generation")
+      endif()
+    endif()
   endif()
 
   # This test has linker issue when cripts is enabled, so its commented for now
diff --git a/plugins/header_rewrite/conditions_geo_maxmind.cc 
b/plugins/header_rewrite/conditions_geo_maxmind.cc
index 5fd905deca..f9168a5a23 100644
--- a/plugins/header_rewrite/conditions_geo_maxmind.cc
+++ b/plugins/header_rewrite/conditions_geo_maxmind.cc
@@ -32,6 +32,31 @@
 
 MMDB_s *gMaxMindDB = nullptr;
 
+enum class MmdbSchema { NESTED, FLAT };
+static MmdbSchema gMmdbSchema = MmdbSchema::NESTED;
+
+// Detect whether the MMDB uses nested (GeoLite2) or flat (vendor) field layout
+// by probing for the nested country path on a lookup result.
+static MmdbSchema
+detect_schema(MMDB_entry_s *entry)
+{
+  MMDB_entry_data_s probe;
+  int               status = MMDB_get_value(entry, &probe, "country", 
"iso_code", NULL);
+
+  if (MMDB_SUCCESS == status && probe.has_data && probe.type == 
MMDB_DATA_TYPE_UTF8_STRING) {
+    return MmdbSchema::NESTED;
+  }
+
+  status = MMDB_get_value(entry, &probe, "country_code", NULL);
+  if (MMDB_SUCCESS == status && probe.has_data && probe.type == 
MMDB_DATA_TYPE_UTF8_STRING) {
+    return MmdbSchema::FLAT;
+  }
+
+  return MmdbSchema::NESTED;
+}
+
+static const char *probe_ips[] = {"8.8.8.8", "1.1.1.1", "128.0.0.1"};
+
 void
 MMConditionGeo::initLibrary(const std::string &path)
 {
@@ -51,9 +76,23 @@ MMConditionGeo::initLibrary(const std::string &path)
   if (MMDB_SUCCESS != status) {
     Dbg(pi_dbg_ctl, "Cannot open %s - %s", path.c_str(), 
MMDB_strerror(status));
     delete gMaxMindDB;
+    gMaxMindDB = nullptr;
     return;
   }
-  Dbg(pi_dbg_ctl, "Loaded %s", path.c_str());
+
+  // Probe the database schema at load time so we know which field paths to
+  // use for country lookups.  Try a few well-known IPs until one hits.
+  for (auto *ip : probe_ips) {
+    int                  gai_error, mmdb_error;
+    MMDB_lookup_result_s result = MMDB_lookup_string(gMaxMindDB, ip, 
&gai_error, &mmdb_error);
+    if (gai_error == 0 && MMDB_SUCCESS == mmdb_error && result.found_entry) {
+      gMmdbSchema = detect_schema(&result.entry);
+      Dbg(pi_dbg_ctl, "Loaded %s (schema: %s)", path.c_str(), gMmdbSchema == 
MmdbSchema::FLAT ? "flat" : "nested");
+      return;
+    }
+  }
+
+  Dbg(pi_dbg_ctl, "Loaded %s (schema: defaulting to nested, no probe IPs 
matched)", path.c_str());
 }
 
 std::string
@@ -74,48 +113,37 @@ MMConditionGeo::get_geo_string(const sockaddr *addr) const
     return ret;
   }
 
-  MMDB_entry_data_list_s *entry_data_list = nullptr;
   if (!result.found_entry) {
     Dbg(pi_dbg_ctl, "No entry for this IP was found");
     return ret;
   }
 
-  int status = MMDB_get_entry_data_list(&result.entry, &entry_data_list);
-  if (MMDB_SUCCESS != status) {
-    Dbg(pi_dbg_ctl, "Error looking up entry data: %s", MMDB_strerror(status));
-    return ret;
-  }
-
-  if (entry_data_list == nullptr) {
-    Dbg(pi_dbg_ctl, "No data found");
-    return ret;
-  }
+  MMDB_entry_data_s entry_data;
+  int               status;
 
-  const char *field_name;
   switch (_geo_qual) {
   case GEO_QUAL_COUNTRY:
-    field_name = "country_code";
+    if (gMmdbSchema == MmdbSchema::FLAT) {
+      status = MMDB_get_value(&result.entry, &entry_data, "country_code", 
NULL);
+    } else {
+      status = MMDB_get_value(&result.entry, &entry_data, "country", 
"iso_code", NULL);
+    }
     break;
   case GEO_QUAL_ASN_NAME:
-    field_name = "autonomous_system_organization";
+    status = MMDB_get_value(&result.entry, &entry_data, 
"autonomous_system_organization", NULL);
     break;
   default:
     Dbg(pi_dbg_ctl, "Unsupported field %d", _geo_qual);
     return ret;
-    break;
   }
 
-  MMDB_entry_data_s entry_data;
-
-  status = MMDB_get_value(&result.entry, &entry_data, field_name, NULL);
   if (MMDB_SUCCESS != status) {
-    Dbg(pi_dbg_ctl, "ERROR on get value asn value: %s", MMDB_strerror(status));
+    Dbg(pi_dbg_ctl, "Error looking up geo string field: %s", 
MMDB_strerror(status));
     return ret;
   }
-  ret = std::string(entry_data.utf8_string, entry_data.data_size);
 
-  if (nullptr != entry_data_list) {
-    MMDB_free_entry_data_list(entry_data_list);
+  if (entry_data.has_data && entry_data.type == MMDB_DATA_TYPE_UTF8_STRING) {
+    ret = std::string(entry_data.utf8_string, entry_data.data_size);
   }
 
   return ret;
@@ -139,45 +167,31 @@ MMConditionGeo::get_geo_int(const sockaddr *addr) const
     return ret;
   }
 
-  MMDB_entry_data_list_s *entry_data_list = nullptr;
   if (!result.found_entry) {
     Dbg(pi_dbg_ctl, "No entry for this IP was found");
     return ret;
   }
 
-  int status = MMDB_get_entry_data_list(&result.entry, &entry_data_list);
-  if (MMDB_SUCCESS != status) {
-    Dbg(pi_dbg_ctl, "Error looking up entry data: %s", MMDB_strerror(status));
-    return ret;
-  }
-
-  if (entry_data_list == nullptr) {
-    Dbg(pi_dbg_ctl, "No data found");
-    return ret;
-  }
+  MMDB_entry_data_s entry_data;
+  int               status;
 
-  const char *field_name;
   switch (_geo_qual) {
   case GEO_QUAL_ASN:
-    field_name = "autonomous_system_number";
+    // GeoLite2-ASN / DBIP-ASN store this as a top-level uint32 field
+    status = MMDB_get_value(&result.entry, &entry_data, 
"autonomous_system_number", NULL);
     break;
   default:
     Dbg(pi_dbg_ctl, "Unsupported field %d", _geo_qual);
     return ret;
-    break;
   }
 
-  MMDB_entry_data_s entry_data;
-
-  status = MMDB_get_value(&result.entry, &entry_data, field_name, NULL);
   if (MMDB_SUCCESS != status) {
-    Dbg(pi_dbg_ctl, "ERROR on get value asn value: %s", MMDB_strerror(status));
+    Dbg(pi_dbg_ctl, "Error looking up geo int field: %s", 
MMDB_strerror(status));
     return ret;
   }
-  ret = entry_data.uint32;
 
-  if (nullptr != entry_data_list) {
-    MMDB_free_entry_data_list(entry_data_list);
+  if (entry_data.has_data && entry_data.type == MMDB_DATA_TYPE_UINT32) {
+    ret = entry_data.uint32;
   }
 
   return ret;
diff --git a/plugins/header_rewrite/generate_test_mmdb.py 
b/plugins/header_rewrite/generate_test_mmdb.py
new file mode 100644
index 0000000000..8cd34ac8a4
--- /dev/null
+++ b/plugins/header_rewrite/generate_test_mmdb.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#  http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+"""
+Generate test MMDB files for header_rewrite geo lookup unit tests.
+
+Two schemas exist in the wild:
+
+  Nested (GeoLite2/GeoIP2/DBIP):  country -> iso_code
+  Flat   (vendor-specific):       country_code (top-level)
+
+This script generates one MMDB file for each schema so the C++ test
+can verify that auto-detection works for both.
+
+Requires: pip install mmdb-writer netaddr
+"""
+
+import os
+import sys
+
+try:
+    from mmdb_writer import MMDBWriter, MmdbU32
+    import netaddr
+except ImportError:
+    print("SKIP: mmdb-writer or netaddr not installed (pip install mmdb-writer 
netaddr)", file=sys.stderr)
+    sys.exit(1)
+
+
+def net(cidr):
+    return netaddr.IPSet([netaddr.IPNetwork(cidr)])
+
+
+def generate_flat(path):
+    """Flat schema: country_code at top level (vendor databases)."""
+    w = MMDBWriter(ip_version=4, database_type="Test-Flat-GeoIP")
+    w.insert_network(
+        net("8.8.8.0/24"), {
+            "country_code": "US",
+            "autonomous_system_number": MmdbU32(15169),
+            "autonomous_system_organization": "GOOGLE",
+        })
+    w.insert_network(
+        net("1.2.3.0/24"), {
+            "country_code": "KR",
+            "autonomous_system_number": MmdbU32(9286),
+            "autonomous_system_organization": "KINX",
+        })
+    w.to_db_file(path)
+
+
+def generate_nested(path):
+    """Nested schema: country/iso_code (GeoLite2, GeoIP2, DBIP)."""
+    w = MMDBWriter(ip_version=4, database_type="Test-Nested-GeoIP2")
+    w.insert_network(
+        net("8.8.8.0/24"), {
+            "country": {
+                "iso_code": "US",
+                "names": {
+                    "en": "United States"
+                }
+            },
+            "autonomous_system_number": MmdbU32(15169),
+            "autonomous_system_organization": "GOOGLE",
+        })
+    w.insert_network(
+        net("1.2.3.0/24"), {
+            "country": {
+                "iso_code": "KR",
+                "names": {
+                    "en": "South Korea"
+                }
+            },
+            "autonomous_system_number": MmdbU32(9286),
+            "autonomous_system_organization": "KINX",
+        })
+    w.to_db_file(path)
+
+
+if __name__ == "__main__":
+    outdir = sys.argv[1] if len(sys.argv) > 1 else "."
+
+    flat_path = os.path.join(outdir, "test_flat_geo.mmdb")
+    nested_path = os.path.join(outdir, "test_nested_geo.mmdb")
+
+    generate_flat(flat_path)
+    generate_nested(nested_path)
+
+    print(f"Generated {flat_path} ({os.path.getsize(flat_path)} bytes)")
+    print(f"Generated {nested_path} ({os.path.getsize(nested_path)} bytes)")
diff --git a/plugins/header_rewrite/header_rewrite_test.cc 
b/plugins/header_rewrite/header_rewrite_test.cc
index 8e92f7ae5f..6f6026e3df 100644
--- a/plugins/header_rewrite/header_rewrite_test.cc
+++ b/plugins/header_rewrite/header_rewrite_test.cc
@@ -22,11 +22,17 @@
 
 #include <cstdio>
 #include <cstdarg>
+#include <cstdlib>
 #include <iostream>
 #include <ostream>
+#include <sys/stat.h>
 
 #include "parser.h"
 
+#if TS_USE_HRW_MAXMINDDB
+#include <maxminddb.h>
+#endif
+
 namespace header_rewrite_ns
 {
 const char PLUGIN_NAME[]     = "TEST_header_rewrite";
@@ -538,6 +544,190 @@ test_tokenizer()
   return errors;
 }
 
+#if TS_USE_HRW_MAXMINDDB
+
+static bool
+file_exists(const char *path)
+{
+  struct stat st;
+  return stat(path, &st) == 0;
+}
+
+static int
+open_test_mmdb(MMDB_s &mmdb, const char *path)
+{
+  int status = MMDB_open(path, MMDB_MODE_MMAP, &mmdb);
+  if (MMDB_SUCCESS != status) {
+    std::cerr << "Cannot open " << path << ": " << MMDB_strerror(status) << 
std::endl;
+    return 1;
+  }
+  return 0;
+}
+
+static std::string
+lookup_country(MMDB_s &mmdb, const char *ip)
+{
+  int                  gai_error, mmdb_error;
+  MMDB_lookup_result_s result = MMDB_lookup_string(&mmdb, ip, &gai_error, 
&mmdb_error);
+
+  if (gai_error != 0 || MMDB_SUCCESS != mmdb_error || !result.found_entry) {
+    return "(lookup_failed)";
+  }
+
+  MMDB_entry_data_s entry_data;
+
+  // Try nested path first (GeoLite2 style)
+  int status = MMDB_get_value(&result.entry, &entry_data, "country", 
"iso_code", NULL);
+  if (MMDB_SUCCESS == status && entry_data.has_data && entry_data.type == 
MMDB_DATA_TYPE_UTF8_STRING) {
+    return std::string(entry_data.utf8_string, entry_data.data_size);
+  }
+
+  // Try flat path (vendor style)
+  status = MMDB_get_value(&result.entry, &entry_data, "country_code", NULL);
+  if (MMDB_SUCCESS == status && entry_data.has_data && entry_data.type == 
MMDB_DATA_TYPE_UTF8_STRING) {
+    return std::string(entry_data.utf8_string, entry_data.data_size);
+  }
+
+  return "(not_found)";
+}
+
+static uint32_t
+lookup_asn(MMDB_s &mmdb, const char *ip)
+{
+  int                  gai_error, mmdb_error;
+  MMDB_lookup_result_s result = MMDB_lookup_string(&mmdb, ip, &gai_error, 
&mmdb_error);
+
+  if (gai_error != 0 || MMDB_SUCCESS != mmdb_error || !result.found_entry) {
+    return 0;
+  }
+
+  MMDB_entry_data_s entry_data;
+  int               status = MMDB_get_value(&result.entry, &entry_data, 
"autonomous_system_number", NULL);
+  if (MMDB_SUCCESS == status && entry_data.has_data && entry_data.type == 
MMDB_DATA_TYPE_UINT32) {
+    return entry_data.uint32;
+  }
+  return 0;
+}
+
+// Test that we can read country codes from a flat-schema MMDB (vendor 
databases
+// where country_code is a top-level string field).
+int
+test_flat_schema()
+{
+  const char *path = getenv("MMDB_TEST_FLAT");
+  if (path == nullptr || !file_exists(path)) {
+    std::cout << "SKIP: flat-schema test mmdb not found (set MMDB_TEST_FLAT)" 
<< std::endl;
+    return 0;
+  }
+
+  std::cout << "Testing flat-schema MMDB: " << path << std::endl;
+  int    errors = 0;
+  MMDB_s mmdb;
+  if (open_test_mmdb(mmdb, path) != 0) {
+    return 1;
+  }
+
+  std::string country = lookup_country(mmdb, "8.8.8.8");
+  if (country != "US") {
+    std::cerr << "FAIL: flat schema 8.8.8.8 expected 'US', got '" << country 
<< "'" << std::endl;
+    ++errors;
+  } else {
+    std::cout << "  PASS: flat 8.8.8.8 country = " << country << std::endl;
+  }
+
+  country = lookup_country(mmdb, "1.2.3.4");
+  if (country != "KR") {
+    std::cerr << "FAIL: flat schema 1.2.3.4 expected 'KR', got '" << country 
<< "'" << std::endl;
+    ++errors;
+  } else {
+    std::cout << "  PASS: flat 1.2.3.4 country = " << country << std::endl;
+  }
+
+  uint32_t asn = lookup_asn(mmdb, "8.8.8.8");
+  if (asn != 15169) {
+    std::cerr << "FAIL: flat schema 8.8.8.8 expected ASN 15169, got " << asn 
<< std::endl;
+    ++errors;
+  } else {
+    std::cout << "  PASS: flat 8.8.8.8 ASN = " << asn << std::endl;
+  }
+
+  // Loopback should not be found
+  country = lookup_country(mmdb, "127.0.0.1");
+  if (country != "(lookup_failed)") {
+    std::cerr << "FAIL: flat schema 127.0.0.1 expected no entry, got '" << 
country << "'" << std::endl;
+    ++errors;
+  } else {
+    std::cout << "  PASS: flat 127.0.0.1 correctly not found" << std::endl;
+  }
+
+  MMDB_close(&mmdb);
+  return errors;
+}
+
+// Test that we can read country codes from a nested-schema MMDB 
(GeoLite2/GeoIP2/DBIP
+// where country is country -> iso_code).
+int
+test_nested_schema()
+{
+  const char *path = getenv("MMDB_TEST_NESTED");
+  if (path == nullptr || !file_exists(path)) {
+    std::cout << "SKIP: nested-schema test mmdb not found (set 
MMDB_TEST_NESTED)" << std::endl;
+    return 0;
+  }
+
+  std::cout << "Testing nested-schema MMDB: " << path << std::endl;
+  int    errors = 0;
+  MMDB_s mmdb;
+  if (open_test_mmdb(mmdb, path) != 0) {
+    return 1;
+  }
+
+  std::string country = lookup_country(mmdb, "8.8.8.8");
+  if (country != "US") {
+    std::cerr << "FAIL: nested schema 8.8.8.8 expected 'US', got '" << country 
<< "'" << std::endl;
+    ++errors;
+  } else {
+    std::cout << "  PASS: nested 8.8.8.8 country = " << country << std::endl;
+  }
+
+  country = lookup_country(mmdb, "1.2.3.4");
+  if (country != "KR") {
+    std::cerr << "FAIL: nested schema 1.2.3.4 expected 'KR', got '" << country 
<< "'" << std::endl;
+    ++errors;
+  } else {
+    std::cout << "  PASS: nested 1.2.3.4 country = " << country << std::endl;
+  }
+
+  uint32_t asn = lookup_asn(mmdb, "8.8.8.8");
+  if (asn != 15169) {
+    std::cerr << "FAIL: nested schema 8.8.8.8 expected ASN 15169, got " << asn 
<< std::endl;
+    ++errors;
+  } else {
+    std::cout << "  PASS: nested 8.8.8.8 ASN = " << asn << std::endl;
+  }
+
+  country = lookup_country(mmdb, "127.0.0.1");
+  if (country != "(lookup_failed)") {
+    std::cerr << "FAIL: nested schema 127.0.0.1 expected no entry, got '" << 
country << "'" << std::endl;
+    ++errors;
+  } else {
+    std::cout << "  PASS: nested 127.0.0.1 correctly not found" << std::endl;
+  }
+
+  MMDB_close(&mmdb);
+  return errors;
+}
+
+int
+test_maxmind_geo()
+{
+  int errors  = 0;
+  errors     += test_flat_schema();
+  errors     += test_nested_schema();
+  return errors;
+}
+#endif
+
 int
 main()
 {
@@ -545,5 +735,11 @@ main()
     return 1;
   }
 
+#if TS_USE_HRW_MAXMINDDB
+  if (test_maxmind_geo()) {
+    return 1;
+  }
+#endif
+
   return 0;
 }

Reply via email to