Skip to content

fix(geoip): preserve overlapping dat tags - #37

Merged
olicesx merged 7 commits into
olicesx:mainfrom
JohnsonRan:fix/geoip-overlapping-tags
Aug 12, 2026
Merged

fix(geoip): preserve overlapping dat tags#37
olicesx merged 7 commits into
olicesx:mainfrom
JohnsonRan:fix/geoip-overlapping-tags

Conversation

@JohnsonRan

@JohnsonRan JohnsonRan commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Problem

V2Ray/MetaCubeX GeoIP datasets can assign one IP range to both a country code and named categories such as CLOUDFLARE. KixDNS previously flattened ranges into one sorted list and returned one country_code, so overlapping named categories matched inconsistently.

Response GeoIP matching also considered only direct A/AAAA records. HTTPS/SVCB responses commonly carry endpoint addresses in ipv4hint and ipv6hint.

Changes

  • index IPv4 and IPv6 ranges independently for each GeoIP tag
  • merge overlapping and adjacent ranges within each tag
  • add matches_tag, matches_any_tag, and multi-tag diagnostics
  • preserve overlapping membership for .dat and JSON sources
  • route request, pipeline-selector, and response GeoIP matchers through tag membership
  • use MMDB as authority for two-letter country codes while still matching named .dat/JSON tags when both sources are configured
  • keep MMDB lookups on the existing Moka cache path
  • accept country_codes as a canonical string array, single string, or comma-separated string; trim values and discard empty entries
  • make the config editor display arrays as comma-separated text and export canonical JSON arrays
  • inspect A/AAAA records and HTTPS/SVCB ipv4hint/ipv6hint values in response IP matchers
  • avoid loading GeoIP databases when configuration uses only private-IP matchers
  • document source precedence and response matching semantics in both READMEs

Matching semantics

  • country codes and named tags are case-insensitive
  • MMDB remains authoritative for two-letter country codes
  • named tags use the .dat/JSON index even when MMDB is also configured
  • response_answer_ip_geoip_country inspects Answers only and requires every IP-bearing Answer record to match
  • alternative hints within one HTTPS/SVCB record use any-match semantics
  • CIDR/private response matchers inspect Answers and Additionals with any-match semantics
  • matchers read existing HTTPS/SVCB hints but do not synthesize or rewrite records

Scope

GeoIP DAT protobuf parsing and CIDR conversion remain owned by main commit aef9d7a. This PR does not replace or duplicate that parser.

Static response construction remains outside this PR.

Validation

  • overlapping country and named-tag membership
  • MMDB country precedence plus DAT named-tag matching
  • cached MMDB matcher path
  • request, pipeline-selector, and response GeoIP matchers
  • scalar, comma-separated, and normalized array country_codes
  • mixed IPv4/IPv6 HTTPS and SVCB hints
  • Answer-vs-Additional scope
  • formatting, library/binary tests, Clippy with warnings denied, and diff checks

@olicesx olicesx left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

The core change is well-implemented: per-tag range indexing, matches_tag / matches_any_tag membership queries, and HTTPS/SVCB hint matching are correct and backed by tests. I verified the range merge + partition_point lookups, the case-normalization chain, and the GeoipPrivate -> is_private_ip simplification (behavior-identical, since GeoIpResult.is_private is is_private_ip(ip) on every construction path). No blocking bugs found.

Three items need a decision before merge, plus a few minor/doc nits:

  1. Named tags silently never match when MMDB is configured. With both geoip_db_path + geoip_dat_path, a geoip_country: ["cloudflare"] rule never fires with no warning - undercutting this PR's headline feature. (inline src/matcher/geoip.rs)
  2. HTTPS/SVCB hints use all() within a single record - a silent behavior change. A Cloudflare HTTPS answer with both v4+v6 hints now fails country-tag matches when the v6 range isn't in the tag (false negative vs main). (inline src/matcher/mod.rs)
  3. Moka cache bypassed in the runtime path - matches_any_tag never touches self.cache; lookup() (the only cache user) has no production callers left. (inline src/matcher/mod.rs)

Minor / docs:

  • The PR description claims the shared DAT parser / field-number dispatch / issue #35 fix, but those are already in main (commit aef9d7a); this PR's diff does not touch geoip_converter.rs / geoip_proto.rs. Please update the description to match the actual scope.
  • deserialize_string_or_vec array branch doesn't trim/filter (string branch does). (inline src/config.rs)
  • README doesn't note that response_answer_ip_geoip_country inspects Answers only, while CIDR/private matchers also inspect Additionals; nor that named tags are DAT-only when MMDB is present.
  • uses_geoip_matchers (engine/utils.rs) still counts GeoipPrivate / ResponseAnswerIpGeoipPrivate, so .dat/MMDB gets loaded even when only private matchers are configured (harmless waste).

Comment thread src/matcher/geoip.rs Outdated
Comment thread src/matcher/mod.rs Outdated
Comment thread src/matcher/mod.rs
Comment thread src/config.rs Outdated
Index V2Ray GeoIP ranges per tag so named categories can overlap country codes. Route request, pipeline, and response matchers through tag membership while preserving MMDB precedence and legacy lookup behavior.
@JohnsonRan
JohnsonRan force-pushed the fix/geoip-overlapping-tags branch from 797b468 to 5f8f063 Compare August 11, 2026 11:46

@olicesx olicesx left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean refactor. Splitting the flat range list into per-tag indexes and separating the "membership" lookup (merged ranges, O(log n)) from the "most-specific" lookup (raw ranges) is the right shape for preserving overlapping tag membership, and the test coverage (MMDB precedence, same-tag nesting, HTTPS/SVCB hints, ALL-vs-ANY scopes) is thorough.

Conclusion: 🟡 CHANGES REQUESTED (1 major, 2 nit)

Findings

🟡 major — most_specific_* misses the narrowest covering range under crossing (non-nested) overlapssrc/matcher/geoip.rs:702 (v6 :721). See inline comment. Particularly relevant because the PR's new named-tag support is exactly where aggregated, non-hierarchical (crossing) ranges are most likely to appear.

nit — README doesn't mention the "≥1 address-bearing Answer required" guardREADME.md:332. ResponseAnswerIpGeoipCountry returns false when no Answer carries an address, which is stricter (and safer) than the literal "every IP-bearing Answer must match" wording; one sentence would document the actual behavior.

nit — empty country_codes silently disables the matchersrc/config.rs:1046 (pre-existing). country_codes: "" / [] deserializes to an empty Vec, and matches_any_tag(ip, &[]) is always false; a config-time warning would help.

Verification: cargo build ✓ · cargo clippy --all-targets -- -D warnings ✓ · cargo test ✓ (lib 128 / bin 6 / integration 9 / doctests 10, 0 failed). Reviewed in a worktree at PR HEAD 90759d6.

Comment thread src/matcher/geoip.rs Outdated
@JohnsonRan

Copy link
Copy Markdown
Contributor Author

Addressed the latest review in 2638cd7:

  • Fixed most_specific_* for crossing/non-nested overlaps by choosing the smallest covering span; updated cross-tag specificity consistently and added IPv4/IPv6 regression tests for [40,55] vs [45,70] at ip=50.
  • Documented that response_answer_ip_geoip_country requires at least one address-bearing Answer.
  • Added a runtime-configuration warning when request, pipeline-selector, or response GeoIP country matchers have empty country_codes and would never match.

Validation passed:

  • cargo fmt -- --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test
  • git diff --check

@olicesx Ready for re-review.

@olicesx olicesx left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — verified on 2638cd7. most_specific_* now scans every covering range and picks the smallest span, so the [40,55]/[45,70] ip=50 case returns (40,55) as expected, and is_more_specific is consistent (span-first). The new test_most_specific_handles_crossing_overlaps locks it in; the README "≥1 address-bearing Answer" wording and the empty-country_codes warning are both addressed.

Conclusion: ✅ APPROVE

cargo build · cargo clippy --all-targets -- -D warnings · cargo test (lib 129 / bin 6 / integration 9 / doctests 10, 0 failed).

@olicesx
olicesx merged commit 2da3a2d into olicesx:main Aug 12, 2026
10 checks passed
@JohnsonRan
JohnsonRan deleted the fix/geoip-overlapping-tags branch August 12, 2026 09:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants