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/32resp./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.hosting—true/1/yes(case-insensitive) marks a datacenter/hosting/proxy range; anything else isfalse.- Blank lines and
#comments are ignored; a header row whose first cell isnetworkis 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 carryvia: "dataset". The summary reportsviaRdap. - 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.