
Wazuh Engine Geo 模块深度解析基于 MaxMind MMDB 的 GeoIP 地理富集与热加载机制【免费下载链接】wazuhWazuh - The Open Source Security Platform. Unified XDR and SIEM protection for endpoints and cloud workloads.项目地址: https://gitcode.com/GitHub_Trending/wa/wazuh本文以 Wazuh Engine 中的geo模块源码位于 src/engine/source/geo为核心系统讲解其如何基于 MaxMind MMDB 数据库实现 GeoIP 地理信息富集包括 GeoLite2 City/ASN 数据库的全生命周期管理、基于远程 Manifest 的自动下载与哈希校验更新、无需重启引擎的热加载机制以及面向解码/规则管线的ILocator查询接口。读完本文你将掌握该模块的架构设计、配置项含义、底层调用链与测试验证方法可直接在 Wazuh Engine 中配置和使用 GeoIP 富集能力。模块定位与总体架构geo模块是 Wazuh EngineWazuh 新一代事件处理引擎内置的 GeoIP 富集组件。它负责管理 MaxMind GeoLite2 数据库City 与 ASN 两类并对外提供统一的 IP 地理位置查询接口。模块的核心能力包括远程同步从远端 ManifestS3 JSON 文件自动发现并下载最新的数据库无需人工干预哈希驱动的增量更新通过 MD5 哈希比对判断数据库是否变化未变化则跳过下载热加载Hot-Reload新数据库在运行时原子替换旧版本全程无需重启引擎查询不中断持久化状态数据库的路径、哈希、生成时间等元数据写入内部 store重启后自动恢复查询接口以ILocator形式对外提供getString、getUint32、getDouble、getAsJson、getAll五种查询方法。README 中的架构图清晰地勾勒了从远程 Manifest 到最终消费方的完整数据流┌──────────────────────────────────────────────────────────┐ │ Remote Manifest (S3) │ │ { generated_at: ..., │ │ city: { url: ...gz, md5: ... }, │ │ asn: { url: ...gz, md5: ... } } │ └──────────────────┬────────────────────────────────────────┘ │ downloadManifest() │ downloadHTTPS() extractMmdbFromGz() ┌──────────────────▼────────────────────────────────────────┐ │ Manager (IManager) │ │ remoteUpsert(manifestUrl, cityPath, asnPath) │ │ ◄── scheduler (periodic, default 6 min) │ │ ┌─────────────────────────────────────────────────┐ │ │ │ m_dbs: mapname, shared_ptrDbHandle │ │ │ │ m_dbTypes: mapType, name │ │ │ │ DbHandle ──► atomicshared_ptrDbInstance │ │ │ │ │ │ │ │ │ DbInstance (immutable) │ │ │ │ MMDB_s (mmapd .mmdb file) │ │ │ │ path, hash, createdAt, type │ │ │ └─────────────────────────────────────────────────┘ │ │ getLocator(Type) → shared_ptrILocator │ │ ┌────────────────┐ ┌────────────────────────┐ │ │ │ IStore │ │ IDownloader │ │ │ │ geo/mmdb/0 │ │ HTTPS gz extract │ │ │ └────────────────┘ └────────────────────────┘ │ └──────────────────────────────────────────────────────────┘ │ getLocator(CITY/ASN) │ ┌──────────▼───────────────────────────────────────────────┐ │ Locator (ILocator) │ │ getString(ip, path) → Resultstring │ │ getUint32(ip, path) → Resultuint32_t │ │ getDouble(ip, path) → Resultdouble │ │ getAsJson(ip, path) → Resultjson::Json │ │ getAll(ip) → Resultjson::Json │ │ Caches: last IP lookup result DB instance ref │ └──────────────────────────────────────────────────────────┘ │ Consumers ┌──────────▼──────────────────────┐ │ builder/enrichment/geo.cpp │ GeoIP enrichment expressions │ builder/opmap/mmdb.cpp │ MMDB helper map operations │ api/geo (HTTP endpoints) │ On-demand IP lookup API └──────────────────────────────────┘整个模块自上而下分为四层远程数据源Manifest .gz归档、核心管理器Manager负责下载、校验、热加载与状态持久化、查询定位器Locator/ILocator负责 MMDB 内存查询、以及消费方builder 富集表达式与 API 端点。数据库类型与生命周期模块仅管理两种数据库类型且任意时刻每种类型只允许存在一个数据库TypeEnumDatabase FileDataCitygeo::Type::CITYGeoLite2-City.mmdbCountry, city, continent, location (lat/lon), postal code, timezoneASNgeo::Type::ASNGeoLite2-ASN.mmdbAutonomous System Number, organization name在接口定义 imanager.hpp 中Type枚举与typeName()/typeFromName()提供了类型与字符串city/asn之间的双向转换这也是后续持久化文档与 API 响应中类型标识的基础。validTypeNames()则用于生成可供校验的合法类型名列表。数据库从落地到可查询经历如下阶段对应Manager::addDbUnsafe见 manager.cpp以文件名为 key 检查m_dbs注册表、以类型为 key 检查m_dbTypes注册表确保类型唯一、文件名不重复创建稳定的DbHandle原子指针持有者再创建不可变的DbInstance构造时即执行MMDB_open以 mmap 方式打开文件失败则抛出异常并中止注册将 handle 与实例发布到注册表此后即可通过getLocator()获取查询器。启动时Manager构造函数会读取内部 store 中geo/mmdb/0文档并尝试为 City/ASN 恢复各自已持久化的数据库实例若文档缺失或字段不完整则记录 DEBUG/WARNING 日志并继续以空状态运行等待后续remoteUpsert填充。远程更新流程 remoteUpsert 详解remoteUpsert(manifestUrl, cityPath, asnPath)是模块的核心入口由调度器周期性触发默认每 6 分钟一次见后文配置。其完整流程源自 manager.cpp 与 imanager.hpp 的接口注释如下下载并解析 Manifest通过IDownloader::downloadManifest()拉取 JSON 并解析为json::Json读取generated_at数据库生成时间戳、/city/url、/city/md5、/asn/url、/asn/md5逐类型判断是否需要更新needsUpdate()从 store 中读取该类型已存的哈希与 Manifest 中的 MD5 比对无存储文档、存储字段缺失、物理文件已被删除、或哈希不一致 → 需要更新哈希一致 → 记录 DEBUG 日志后提前返回early-exit不做任何下载下载与 MD5 校验processDbEntry()以MAX_RETRIES最多 3 次循环下载.gz每次下载后用base::utils::hash::md5()计算内容哈希并与 Manifest 期望值比对不匹配则重试并记录错误超过重试次数返回MD5 mismatch错误解压落盘通过IDownloader::extractMmdbFromGz()先将内容写入临时.gz文件再用 zlibHelper 的gzipDecompress解压到path.tmp临时.mmdb文件解压失败时清理临时文件原子替换std::filesystem::rename(tmpPath, path)将临时文件原子重命名为最终路径并将文件权限设为640owner rw、group r、other 无权限热加载基于新文件创建新的不可变DbInstance重新MMDB_open通过DbHandle::store()原子替换旧实例详见下节持久化调用upsertStoreEntry()将新的 path、hash、generated_at 以及本次同步时间last_successful_update写入 store 文档优雅取消在下载、重试、类型切换等每个操作间隙都会检查m_shouldRunstd::atomicbool标志一旦请求停机立即中止剩余流程。值得注意的细节Manifest 下载失败时模块会将该类型状态标记为FAILED但保留旧版本数据库继续提供查询available与hash不变体现旧数据可用性优先的容错设计。网络下载与解压实现Downloaderdownloader.cpp通过共享 HTTP 客户端HTTPRequest发起 GET 请求支持超时配置与shouldRun取消标志下载进行中请求停机时立即中断Manifest 解析失败会返回 Failed to parse manifest JSON 错误。解压阶段通过 zlibHelper 完成 gzip 解压并负责自动创建目标父目录、清理中间临时文件。热加载机制无重启更新数据库热加载是geo模块最精巧的设计由三个内部私有类协作完成ClassFilePurposeDbInstancedbInstance.hpp不可变 RAII 封装构造时MMDB_open内存映射析构时MMDB_close持有 path、hash、createdAt、type禁止拷贝与移动DbHandledbHandle.hpp原子指针持有者std::atomic_load/std::atomic_store操作shared_ptrconst DbInstance实现无锁热替换Locatorlocator.hpp实现ILocator持有weak_ptrDbHandle缓存最近一次 IP 查询结果自动感知数据库实例变化并失效缓存热加载的五步流程README 明确定义新.mmdb先写入临时文件再原子重命名到最终路径创建新的DbInstancemmap 打开已替换的文件DbHandle::store()原子交换shared_ptr发布新实例既有Locator在下一次查询时发现缓存的DbInstance指针与DbHandle::load()不一致见 locator.cpp 的validateAndGetDb()随即清空 IP 查询缓存并切换到新实例旧DbInstance在所有引用释放后被销毁MMDB_close。由于替换是原子的查询线程永远不会看到半新半旧的状态要么继续用旧库要么立即用新库。旧库内存仅当所有Locator不再引用时才会释放因此并发查询天然安全。ILocator 查询接口与 MMDB 取值ILocatorilocator.hpp定义了五种查询方法path参数使用与 MMDB 内部结构一致的点分路径dot notation例如country.names.en、location.latitudeMethodReturnDescriptiongetString(ip, path)Resultstring获取点分路径处的字符串值如country.names.engetUint32(ip, path)Resultuint32_t获取 uint32 值如 ASN 编号getDouble(ip, path)Resultdouble获取 double 值如纬度/经度getAsJson(ip, path)Resultjson::Json获取指定路径的值并转为 JSONgetAll(ip)Resultjson::Json获取该 IP 的完整记录JSON 对象每次查询的内部调用链locator.cpp为validateAndGetDb()校验句柄并检查缓存失效→lookup()调用 libmaxminddb 的MMDB_lookup_string解析 IP→getEData()调用MMDB_aget_value按点分路径取条目→ 类型检查并返回。其中lookup()依赖per-locator 的 IP 缓存同一 IP 重复查询直接复用MMDB_lookup_result_s避免重复解析getEData()在found_entry为假时返回IP_NOT_FOUNDgetAll()通过MMDB_get_entry_data_list取出完整条目链表再由dumpEntryDataList递归将 map/array/string/bytes/double/float/uint16/uint32/boolean/uint64/uint128/int32 等 MMDB 数据类型逐项转换为 JSON每个方法在取值后都会做类型严格校验getString要求MMDB_DATA_TYPE_UTF8_STRINGgetUint32要求MMDB_DATA_TYPE_UINT32getDouble要求MMDB_DATA_TYPE_DOUBLE类型不符即返回对应类型不匹配错误码。需要说明的是getAsJson不支持数组与对象类型的路径值会返回DATA_TYPE_MISMATCH_SIMPLE标量类型含 bytes、uint128 等均会被转换为合适的 JSON 表示。错误处理Result 与类型化错误码模块不使用字符串错误而是自定义了ResultT类型语义类似 C23 的std::expected见 errorCodes.hpp配合类型化ErrorCode枚举支持isSuccess()/isError()/value()/error()及operator bool等便捷操作并特化了Resultvoid即VoidResult。getErrorDescription()为每个错误码提供人类可读描述operator使错误码可直接用于断言与日志。错误类别与错误码总览CategoryCodesDatabaseDB_NOT_AVAILABLE、DB_HANDLE_EXPIRED、DB_TYPE_NOT_AVAILABLEIP/NetworkIP_TRANSLATION、IP_NOT_FOUNDData accessDATA_TYPE_MISMATCH、DATA_TYPE_MISMATCH_SIMPLE、DATA_TYPE_MISMATCH_STRING、DATA_TYPE_MISMATCH_UINT32、DATA_TYPE_MISMATCH_DOUBLE、DATA_ENTRY_EMPTYMMDB libraryMMDB_VALUE_ERROR、MMDB_LIBMMDB_ERROR、MMDB_RETRIEVAL_ENTRY_LIST、MMDB_DUMP_ENTRYGeneralUNKNOWN_ERROR例如某类型没有加载任何数据库时getLocator()返回DB_TYPE_NOT_AVAILABLEIP 字符串无法被getaddrinfo解析时返回IP_TRANSLATIONIP 合法但在库中无记录时返回IP_NOT_FOUND。配置项与集成方式模块的全部配置键定义在 conf/include/conf/keys.hpp默认值注册在 conf/src/conf.cppKeyEnv OverrideDefaultDescriptionanalysisd.geo_sync_intervalWAZUH_GEO_SYNC_INTERVAL360同步检查间隔秒0表示禁用analysisd.geo_db_pathWAZUH_GEO_DB_PATH{wazuhRoot}/data/mmdb.mmdb文件存放目录analysisd.geo_manifest_urlWAZUH_GEO_MANIFEST_URLS3 URLManifest JSON 的下载地址analysisd.geo_download_timeoutWAZUH_GEO_DOWNLOAD_TIMEOUT60000HTTP 下载超时毫秒集成代码位于引擎入口README 中的main.cpp集成示例关键步骤如下// 1. Create downloader with timeout and manager with store auto geoDownloader std::make_sharedgeo::Downloader(geoDownloadTimeout); geoManager std::make_sharedgeo::Manager(store, geoDownloader); geoDownloader-setShouldRun(geoManager-shouldRunFlag()); // 2. Shutdown hook exitHandler.add([geoManager]() { geoManager-requestShutdown(); }); // 3. Register API endpoints api::geo::handlers::registerHandlers(geoManager, apiServer); // 4. Inject into builder for enrichment builderDeps.geoManager geoManager; // 5. Schedule periodic sync scheduler-scheduleTask(geo-sync-task, { .interval geoSyncInterval, .runImmediately true, .taskFunction [geoManager, manifestUrl, cityPath, asnPath]() { geoManager-remoteUpsert(manifestUrl, cityPath, asnPath); } });要点解读调度器geo-sync-task以geoSyncInterval为周期运行runImmediately true表示引擎启动后立即执行首次同步无需等待第一个周期geoSyncInterval即来自analysisd.geo_sync_interval默认 360 秒 6 分钟停机钩子exitHandler注册requestShutdown()停机时置位共享的shouldRun原子标志正在进行的下载/同步会被及时取消注入 builderbuilderDeps.geoManager将管理器注入富集构建器使解码/规则管线可以调用 GeoIP 富集表达式。消费方富集表达式与 API 端点模块的消费方主要有三处README Consumers 表格构成完整的使用闭环ModuleUsagebuilder/enrichmentgetLocator(CITY)getLocator(ASN)构建解码/规则管线中的 GeoIP 富集表达式builder/opmap在 MMDB 映射操作中调用getLocator()做事件字段富集api/geoPOST /_internal/geo/db/getIP 查询与POST /_internal/geo/db/list列出已加载数据库main.cpp通过调度器周期性执行remoteUpsert富集侧的实现位于 builder/src/builders/enrichment/geo.cpp它定义了Geo()与AS()富集表达式trace 名称enrichment/Geo配置文档集合为enrichment/geo_mapping/0通过MappingConfig将事件中的源 IP 字段映射到 ECS 风格的 Geo/AS 输出路径并根据查询结果输出不同的成功/失败 trace如 Geo(ip) - Success: Geo enrichment applied。opmap 侧的 MMDB 映射操作实现位于 builder/src/builders/opmap/mmdb.cpp单元测试可参考 mmdb_test.cpp使用geo::mocks::MockManager隔离真实数据库。API 侧的注册代码位于 api/geo/include/api/geo/handlers.hppPOST /_internal/geo/db/get与POST /_internal/geo/db/list处理实现见 api/geo/src/handlers.cppgetDb会分别通过getLocator(CITY)与getLocator(ASN)获取两个定位器调用getAll(ip)返回城市与 ASN 两段 JSONIP 为空时返回用户错误listDb则映射IManager::listDbs()输出当前已加载的数据库列表。该端点对应 API 文档 api/README.md 中的 Geo 路由表。线程安全模型模块对并发访问做了分层设计README Thread Safety 章节均可在源码中得到印证注册表m_dbs、m_dbTypes由std::shared_mutex保护——getLocator()/listDbs()使用共享锁并发读processDbEntry()/addDbUnsafe使用独占锁写DbHandle通过std::atomic_load/std::atomic_store对shared_ptrconst DbInstance做无锁原子交换dbHandle.hpp热加载不阻塞查询线程Locator每个实例并非线程安全要求每个消费方各自持有一个LocatorIP 查询结果按 locator 独立缓存停机std::atomicbool标志在Manager与Downloader之间共享支持传输中途取消。这一分层使得更新线程写、查询线程读的典型并发场景下查询路径几乎无锁开销只有注册表访问与原子指针交换。持久化内部 store 中的 geo/mmdb/0数据库元数据以单文档形式存储在内部 store 的geo/mmdb/0键下README Persistence 章节示例{ city: { path: /var/wazuh-manager/data/mmdb/GeoLite2-City.mmdb, hash: abc123..., generated_at: 1715270400 }, asn: { path: /var/wazuh-manager/data/mmdb/GeoLite2-ASN.mmdb, hash: def456..., generated_at: 1715270400 } }Manager 构造时读取该文档并尝试打开其中引用的.mmdb文件对应 manager.cpp 的checkAndLoadDb每次更新只改写发生变更类型的字段并额外写入last_successful_update本地同步时间戳使状态在重启后仍可恢复。upsertStoreEntry()使用doc.setString/doc.setInt64按/city/...、/asn/...前缀更新字段再通过m_store-upsertDoc()落盘。文件结构、测试与基准模块目录结构README File Structure如下geo/ ├── CMakeLists.txt # Build: igeo (INTERFACE), geo (STATIC), mocks, tests, benchmark ├── interface/geo/ │ ├── imanager.hpp # IManager 接口listDbs, remoteUpsert, getLocator, requestShutdown │ ├── ilocator.hpp # ILocator 接口getString, getUint32, getDouble, getAsJson, getAll │ ├── idownloader.hpp # IDownloader 接口downloadHTTPS, downloadManifest, extractMmdbFromGz │ └── errorCodes.hpp # ErrorCode 枚举 ResultT 类型 ├── include/geo/ │ ├── manager.hpp # Manager 类声明 │ └── downloader.hpp # Downloader 类声明 ├── src/ │ ├── manager.cpp # Manager 实现remoteUpsert, processDbEntry, store 持久化 │ ├── downloader.cpp # HTTP 下载 gz 解压 │ ├── locator.cpp # Locator 实现MMDB 查询 │ ├── locator.hpp # Locator 类声明私有 │ ├── dbHandle.hpp # 原子 shared_ptr 持有者热加载 │ └── dbInstance.hpp # 不可变 MMDB_s RAII 封装 ├── test/ │ ├── mocks/geo/ │ │ ├── mockManager.hpp # GMock 的 IManager mock │ │ └── mockLocator.hpp # GMock 的 ILocator mock │ └── src/ │ ├── testdb.mmdb # 测试用 MMDB 数据库 │ ├── generateTestDB.pl # 测试数据库再生成脚本 │ ├── mockDownloader.hpp # IDownloader mock测试用 │ ├── unit/ │ │ ├── manager_test.cpp # Manager 单元测试mock store downloader │ │ └── locator_test.cpp # Locator 单元测试真实测试 MMDB │ └── component/ │ └── manager_test.cpp # 组件测试端到端更新流程 └── benchmark/src/ └── geo_bench.cpp # MMDB 查询性能基准测试与基准覆盖README Testing 章节测试目标覆盖内容geo_utest单元测试manager_test.cpp从 store 构造、addDb、带 mock downloader 的remoteUpsert、哈希比对、错误处理locator_test.cpp五种查询方法、缓存失效、非法 IP/路径的错误码geo_ctest组件测试manager_test.cpp使用真实 MMDB 文件的端到端更新流程geo_benchmark基准geo_bench.cpp每次查询的 MMDB 查找延迟构建与运行方式基于 CMakeLists.txt 的目标定义# Tests make --directory$WAZUH_REPO/src -j TARGETmanager ENGINE_TESTy DEBUGyes $ENGINE_BUILD/source/geo/geo_utest $ENGINE_BUILD/source/geo/geo_ctest # Benchmarks (requires ENGINE_BUILD_BENCHMARKON) $ENGINE_BUILD/source/geo/geo_benchmark此外模块提供的 GMock mockgeo::mocks::MockManager与geo::mocks::MockLocatorCMake 目标geo::mocks被builder与api/geo的测试目标复用测试时无需真实 MMDB 文件即可验证上层逻辑。总结Wazuh Engine 的geo模块是一个小而完整的 GeoIP 基础设施它以 MaxMind MMDB 为数据载体用DbInstance不可变 mmap 实例DbHandle原子指针Locator带缓存与失效检测的查询器三层结构实现了无锁热加载用 Manifest MD5 哈希比对 最多 3 次重试实现了精准、容错的远程增量更新用ResultT 类型化ErrorCode实现了可读、可断言的错误处理并通过 builder 富集表达式与/_internal/geo/*HTTP 端点将能力开放给引擎的规则管线和运维侧。理解该模块的设计对于在 Wazuh Engine 中启用地理富集、排查 GeoIP 数据同步问题乃至借鉴其热加载与状态持久化模式都有直接的参考价值。【免费下载链接】wazuhWazuh - The Open Source Security Platform. Unified XDR and SIEM protection for endpoints and cloud workloads.项目地址: https://gitcode.com/GitHub_Trending/wa/wazuh创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考