GeoIP and owner classification
Two lookups enrich the query logs:
- GeoIP maps an address to a country (
internal/geoip). It gives each answer address of thedestinationsanalytics itscountry. - Owner classification names the company behind a query or an
address (
internal/classify): Google, Apple, Meta, Amazon and Microsoft, and the CDNs Cloudflare, Akamai and Fastly. It gives each destination itsownerand feeds thetop_ownersanalytics shape.
The GeoIP database
Section titled “The GeoIP database”The control plane reads one country database in the MaxMind DB format
(.mmdb). Production uses DB-IP IP to Country Lite (decided
2026-09-30), refreshed weekly by the geoip role
(below). The code reads any of three free
databases that share the format:
| Database | Licence | Attribution the licence asks for |
|---|---|---|
| MaxMind GeoLite2 Country | GeoLite2 EULA (account and licence key) | This product includes GeoLite2 data created by MaxMind |
| DB-IP IP to Country Lite | CC BY 4.0 | IP Geolocation by DB-IP, with a link to db-ip.com |
| IPinfo Lite | CC BY-SA 4.0 | IP address data powered by IPinfo |
A lookup returns the record’s country, or its registered country when it has none (anycast and satellite ranges), as an upper-case ISO 3166-1 alpha-2 code. Addresses that cannot have a country are never looked up: private, CGNAT (100.64.0.0/10), loopback, link-local, unique-local, documentation and benchmarking ranges.
Tests and the simulation never download anything: the simulation
publishes a small test database
(internal/geoip/testdata/test-country.mmdb) through the same role.
Refresh and distribution
Section titled “Refresh and distribution”opdns-cp geoip run is a long-running role (one replica is enough) that
keeps the published database current:
- Fetch. Once a week (
--geoip-interval, 168 h) it downloads the month’s DB-IP file over HTTPS (https://download.db-ip.com/free/dbip-country-lite-{YYYY}-{MM}.mmdb.gz,--geoip-dbip-url). DB-IP publishes a new file at the start of each month; until it appears, the previous month’s is used. A failed run is retried after an hour (--geoip-retry-interval). - Verify. The file must open as a country database and answer the
probes (
--geoip-probes, default8.8.8.8=US), and it must not be older than the one already published. A file that fails any check is refused and the published one stays. - Publish. It writes the database to the opdns bucket as
geoip/country.mmdb(--geoip-object), keeps the one it replaces asgeoip/country.previous.mmdb, and writes the sidecargeoip/country.json: checksum, size, source, build date, attribution, when it was published and when it was last confirmed current. A run that finds the same file only updates the sidecar’s check time. On a single host,--out PATHwrites a local file instead of, or as well as, the bucket. - Sync. Every
apiandingestreplica started withOPDNS_GEOIP_OBJECT=geoip/country.mmdbreads the sidecar everyOPDNS_GEOIP_SYNC_INTERVAL(10 minutes), downloads the database when its checksum changed, checks it, writes it atomically to its localOPDNS_GEOIP_DBand reloads. A replica that starts before anything is published runs without countries until the first sync.
The same binary has one-shot subcommands, which take the same flags:
opdns-cp geoip status # the published sidecar: build date, checksum, last check, pinnedopdns-cp geoip refresh # fetch, verify and publish nowopdns-cp geoip refresh --source file --source-file ./dbip-country-lite.mmdb.gzopdns-cp geoip rollback # make the previous database current again, pinnedopdns-cp geoip refresh --force # publish over a pinned or newer database--source file publishes a file obtained by other means (a DB-IP file
downloaded by hand when DB-IP is unreachable from the control plane, or a
MaxMind or IPinfo database). rollback swaps the current and previous
databases and pins the result: the weekly run then leaves it alone
(and fails, so the alert below eventually fires) until someone runs
refresh --force. In the local simulation, mise run dev:geoip -- status|refresh|rollback runs the same commands against the stack’s
bucket, with the test fixture as the source.
Alert and metrics
Section titled “Alert and metrics”| Metric | On | Meaning |
|---|---|---|
opdns_cp_geoip_last_success_timestamp_seconds |
geoip role |
when a refresh last published or confirmed the current database |
opdns_cp_geoip_build_timestamp_seconds |
geoip role |
build time of the published database |
opdns_cp_geoip_refresh_runs_total{result} |
geoip role |
runs by published, unchanged or error |
opdns_geoip_build_timestamp_seconds |
api, ingest |
build time of the database the replica has loaded |
opdns_geoip_loaded_timestamp_seconds |
api, ingest |
the replica’s last load |
opdns_geoip_reload_errors_total |
api, ingest |
files that failed to load (the previous one stays in use) |
GeoIPStale (warn) fires when no refresh has succeeded for 45 days;
with a weekly schedule and monthly DB-IP files, a healthy database is
never more than about 38 days old. A role that has never succeeded counts
as stale. Its runbook, docs/fleet/runbooks/geoip-refresh.md, covers DB-IP
being unreachable, wrong countries after a refresh (roll back) and
replicas that do not pick up a new file. The alert’s rules and tests are
in deploy/dev/prometheus/rules/geoip.rules.yml; how alerts are routed is
on Alerting.
Configuration of the api, ingest and link roles
Section titled “Configuration of the api, ingest and link roles”The link role uses the database to place each self-hosted node where it
links from, for its owner’s Analytics globe (the node’s location); it
also takes OPDNS_TRUSTED_PROXIES, the load balancers whose
X-Forwarded-For gives the node’s address.
| Variable (flag) | Default | Meaning |
|---|---|---|
OPDNS_GEOIP_DB (--geoip-db) |
none | the local .mmdb path; empty or none for no database |
OPDNS_GEOIP_OBJECT (--geoip-object) |
none | the published key to sync into OPDNS_GEOIP_DB, usually geoip/country.mmdb; needs the role’s S3 settings |
OPDNS_GEOIP_SYNC_INTERVAL (--geoip-sync-interval) |
10 minutes | how often the sidecar is checked |
OPDNS_GEOIP_ATTRIBUTION (--geoip-attribution) |
empty | the notice in GET /v1/meta: empty uses the published one (else the database type’s), none shows none, anything else is shown as is |
- Without
OPDNS_GEOIP_OBJECT, a configured file must exist and load at start, or the role exits. With it, a missing file is fine until the first sync. - Either way the replica checks the file every 10 minutes and reloads it when its size or modification time changed, so moving a new file into place needs no restart. A file that fails to load keeps the previous database in use.
- The local path must be writable when syncing; the images run as a
non-root user, so use the allocation directory, an
emptyDiror/tmp.
Without a database, every country is "": the API still returns the
country column, and GET /v1/capabilities reports the
destinations_map feature off.
Attribution
Section titled “Attribution”DB-IP’s Lite licence (CC BY 4.0) requires credit wherever its data is
shown. The refresh publishes the notice with the database, IP Geolocation
by DB-IP (https://db-ip.com), licensed under CC BY 4.0., and the API
returns the loaded database’s notice as geoip_attribution in
GET /v1/meta: the published one, or else the one the database’s type
asks for (GeoLite2, DB-IP or IPinfo above), or "" for any other type and
without a database. The dashboard shows it beside the
destinations globe, with
its URLs as links. While DB-IP data is served the notice must not be
empty, so do not set OPDNS_GEOIP_ATTRIBUTION=none in production.
Owner classification
Section titled “Owner classification”The classifier’s data is checked into the repository under
data/classify/ and compiled into both the control plane and the
self-hosted node, so both classify the same way with no configuration:
| File | Holds |
|---|---|
domains.txt |
domain suffixes each company operates itself (youtube.com google, github.com microsoft, brand top-level domains such as google); the longest match wins. Curated by hand |
asns.txt |
each owner’s autonomous systems |
prefixes/<owner>.txt |
each owner’s address ranges, aggregated, with their sources and fetch date in the header |
Where the ranges come from:
| Owner | Source |
|---|---|
its published list (goog.json) minus Google Cloud’s customer ranges (cloud.json) |
|
| Amazon | its published ip-ranges.json |
| Cloudflare | its published IPv4 and IPv6 lists |
| Fastly | its published public IP list |
| Apple, Meta, Microsoft, Akamai | RIPEstat’s announced prefixes of their ASNs |
go run data/classify/refresh.go refetches the ranges; it is run by hand
and never by tests or builds, and its diff is reviewed before it is
committed. Changes to data/classify/ need code-owner review. Tests pin
well-known names and addresses (8.8.8.8 is Google’s), so a refresh that
drops one shows up in review.
A query’s owner is its name’s owner (from domains.txt), or else the
owner of its first answer address that falls in a known range. A
destination’s owner is its address’s, by range only. Customers of a
CDN are classified by address: a site behind Cloudflare counts as
Cloudflare.
What is stamped at ingest
Section titled “What is stamped at ingest”For cloud-stored logs, ingest adds three columns to each ClickHouse row
(schema migration 0005_geo_owner), with the database and classifier
data in use at that moment:
| Column | Holds |
|---|---|
answer_country |
the country of each answer address, in the order of answer_ips ("" when unknown) |
client_country |
the client address’s country, only when the profile logs client IP addresses; "" otherwise |
owner |
the query’s owner, "" when none |
Two hourly rollups follow from them: queries per owner (owners_hourly,
which top_owners reads for whole-hour ranges) and answer addresses per
country (countries_hourly). Like the other rollups, each expires with
the rows it summarises, and both are erased with the account’s logs.
The values are frozen when the row is written: a new GeoIP database or a
data update changes new rows only. Rows written before the migration read
as "", and the API fills an empty country from its current database when
it answers destinations. A destination’s owner is not read from the
row: the API classifies each address when it answers, with the data it
ships with.
Self-hosted nodes have no GeoIP database. A node classifies owners
itself, at query time, from the data built into its release; its
destinations rows carry an empty country, which the API fills from its
own database as the answer passes through to the dashboard
(Analytics from your node).
A node older than these columns gets both added by the API.
Privacy
Section titled “Privacy”- Client countries follow the client IP switch. With Log client IP addresses off, the edge removes the address on the PoP, and ingest never looks up a country for the client, even when a record carries an address. With it on, the country is stored alongside the address, for the same retention. No API view returns client countries today.
- Domain logging off means no answer countries and no owner. Such a record has no name and no answer addresses, so there is nothing to look up.
- Lookups are local. The database is a file on the server and the classifier’s data is compiled in; no address is sent to a third party.
- Retention and deletion. The new columns live in rows that expire per the profile’s retention; the two rollups expire with them, and account deletion erases both along with the other log tables.