Skip to content

IP enrichment (enrich_ips)

Traffic / abuse / security investigations over access logs need the same three things for each client IP: geo (country/city), ASN/org (which network/ISP), and a hosting/proxy flag (the signal that separates real humans from datacenter IPs, scanners, and VPN exit nodes). The enrich_ips tool resolves a batch of IPs to those fields from a local, offline dataset — there is no per-IP call to an external geo API, so it works in air-gapped deployments.

Enabling

Point the server at a local CSV:

bash OMCP_IP_ENRICH_FILE=/data/ip-enrichment.csv

When unset (the default), enrich_ips is still advertised but returns a clear "not configured" notice — no external lookups ever happen implicitly.

Dataset format

A dependency-free CSV (so no parser library / host npm install is needed). One row per network (IPv4 or IPv6):

csv network,country,city,asn,org,hosting 1.2.3.0/24,US,Ashburn,AS14618,Example Cloud,true 203.0.113.5,DE,Berlin,AS3320,Example ISP,false 2001:db8::/32,US,,AS14618,Example Cloud,true

  • network — an IPv4 or IPv6 CIDR, or a bare address (treated as /32 resp. /128). Both families are supported; lookups dispatch by address family. IPv4-mapped IPv6 (::ffff:1.2.3.4) parses on the IPv6 side.
  • country, city, asn, org — optional; an empty cell is omitted from the result.
  • hostingtrue/1/yes (case-insensitive) marks a datacenter/hosting/proxy range; anything else is false.
  • Blank lines and # comments are ignored; a header row whose first cell is network is skipped.

Overlapping/nested ranges resolve to the most specific (longest prefix) match, so a /24 row wins over a /8 that also contains the IP. The dataset is loaded once at boot and looked up in-memory.

You supply the data offline — e.g. export the ranges of interest from whatever geo/ASN source you already license into this CSV and mount it. This keeps enrichment air-gapped and avoids bundling any third-party database into the image.

Building the CSV from a MaxMind GeoLite2 export

scripts/build-ip-enrich-csv.mjs reshapes a licensed MaxMind GeoLite2 export into this CSV. It runs with plain node (no npm install), handles IPv4 and IPv6, and joins City blocks → country/city via the Locations file and (optionally) ASN blocks → asn/org. The geo/ASN data never leaves your machine — the script only reformats a file you already license; nothing is bundled and there is no network call.

```bash node scripts/build-ip-enrich-csv.mjs \ --city-blocks GeoLite2-City-Blocks-IPv4.csv \ --city-blocks GeoLite2-City-Blocks-IPv6.csv \ --locations GeoLite2-City-Locations-en.csv \ --asn-blocks GeoLite2-ASN-Blocks-IPv4.csv \ --asn-blocks GeoLite2-ASN-Blocks-IPv6.csv \ --out enrich.csv

then: OMCP_IP_ENRICH_FILE=enrich.csv

```

--locations and --asn-blocks are optional (with only city blocks you get network,country,city). The hosting flag is left blank — GeoLite2 City/ASN don't carry it (it lives in the paid Anonymous-IP DB); set it yourself if you have that data.

Usage

jsonc { "ips": ["203.0.113.5", "198.51.100.9", "1.2.3.99"] }

Returns one row per input IP (max 1000 per call); unmatched and invalid IPs come back with found: false rather than failing the batch:

jsonc { "results": [ { "ip": "1.2.3.99", "found": true, "country": "US", "city": "Ashburn", "asn": "AS14618", "org": "Example Cloud", "hosting": true }, { "ip": "8.8.8.8", "found": false } ], "summary": { "total": 2, "matched": 1, "unmatched": 1, "invalid": 0 }, "datasetSize": 1 }

A typical agent flow: pull the IPs of interest with query_logs (use labels / aggregate to filter or top-k first), then pass them to enrich_ips to answer "where from / which are bots".

Air-gapped guarantee

By default enrich_ips never makes an outbound request — all data comes from the local file. This is the deliberate trade vs. a live geo-API enrichment on every log line, which would break the air-gapped model.

Optional online RDAP fallback (non-air-gapped)

For deployments that aren't air-gapped, building a MaxMind dataset is a lot of ceremony just to answer "where is this IP / is it a datacenter". You can instead enable an opt-in online fallback that queries the authoritative RIR via RDAP (RFC 9082/9083) over HTTPS:

```bash OMCP_IP_ENRICH_RDAP=on # also accepts true / 1

optional: override the bootstrap endpoint (default https://rdap.org)

OMCP_IP_ENRICH_RDAP_URL=https://rdap.org

```

  • OFF by default — the air-gapped guarantee above holds unless you set this. No external call is ever made otherwise.
  • The offline dataset is always preferred. RDAP is only consulted for IPs the dataset didn't cover (or when no dataset is configured). Hits carry via: "rdap"; dataset hits carry via: "dataset". The summary reports viaRdap.
  • Returns country + org only — RDAP carries no city-level geo and no hosting/proxy flag (the org name is the bot-vs-human signal). For city precision, use the offline GeoLite2 dataset.
  • Privacy: RDAP queries the authoritative registry, not a third-party geo broker — the cleanest posture for looked-up visitor IPs.
  • Results are cached with a TTL (default 1h; misses cached briefly) to respect RIR rate limits; a flaky RIR returns a miss, never an error.