curl ip.12388321.xyz and find out.
Self-hosted IP geolocation + IP type classification. Hit the root URL and it auto-detects your public IP, returns geolocation data (country, city, lat/lon, ASN), IP type classification (residential/datacenter/vpn/tor/proxy), and proxy flags (mobile/proxy/hosting).
docker compose up -d
# Visit http://localhost:8081/ or point your domain at it
curl http://localhost:8081/- You visit the site
- App detects your real public IP from Cloudflare headers (
CF-Connecting-IP/X-Real-IP/X-Forwarded-For) - Looks it up in three local MMDB databases (auto-downloaded on first start, cached in a Docker volume)
- Enriches with ip.pn API for IPv4 proxy/mobile/hosting flags (no rate limit, no auth)
- Checks dan.me.uk Tor exit list for fastest and most comprehensive Tor node detection
- Derives IP type from combined signals across all sources (dan.me.uk → IP66 → Umkus → ip.pn priority)
- Returns JSON with geolocation + classification
IPv4 and IPv6 both supported — the app detects whichever IP version you connect on.
We use five data sources, each contributing different fields. Here's exactly what we use from each:
- License: CC BY 4.0
- URL:
https://downloads.ip66.dev/db/ip66.mmdb - Size: ~17 MB
- Updates: Daily
- What we use:
country.iso_code→country_codecountry.names.en→country_namecontinent.code→continent_codecontinent.names.en→continent_nameautonomous_system_number→asn(formatted asAS{n})autonomous_system_organization→as_orgrir→rir(Regional Internet Registry — ARIN/RIPE/APNIC/etc.)anonymous_ip.is_tor_exit_node— used in IP type derivationanonymous_ip.is_anonymous_vpn— used in IP type derivationanonymous_ip.is_public_proxy— used in IP type +proxyflaganonymous_ip.is_hosting_provider— used in IP type +hostingflag
-
License: CC BY-SA 4.0 (MaxMind)
-
URL:
https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-City.mmdb -
Size: ~64 MB
-
Updates: Every Tuesday and Friday
-
What we use:
city.names.en→citylocation.latitude→latitudelocation.longitude→longitudelocation.time_zone→timezonelocation.accuracy_radius→accuracy_radius(in km)postal.code→postal_codesubdivisions[].names.en→region(state/province)country.iso_code→country_code(takes priority over IP66 for physical location accuracy)country.names.en→country_namecontinent.code→continent_codecontinent.names.en→continent_nameregistered_country.iso_code→registered_country(registry country — useful for VPNs/proxies where physical ≠ registry)traits.autonomous_system_number— backfill for ASN if IP66 misses it
Why GeoLite2 overrides IP66 for country: IP66's country data is based on IP registry/allocation (e.g., who the IP block was assigned to via RIPE/ARIN/APNIC). This often differs from the physical location for VPNs, proxies, and anycast IPs. GeoLite2 does actual IP geolocation, so its country/continent data reflects the real physical location. The registry country is still preserved in the
registered_countryfield.Why P3TERX mirror instead of official: This mirror provides a single MMDB file containing both IPv4 and IPv6 data. The official MaxMind distribution and most mirrors split them into separate files.
- License: GPL 3.0
- URL:
https://github.com/Umkus/ip-index/releases/download/latest/ip-index.mmdb - Size: ~13 MB
- Updates: Daily (automated builds)
- What we use:
hosting(0/1 flag) — used in IP type derivation +hostingflagasn_name— backfill foras_orgif IP66 has no org name
-
License: Free API (no auth, no rate limit)
-
URL:
https://ip.pn/json/{IP} -
What we use:
mobile→mobilebooleanproxy→proxybooleanhosting→hostingboolean
Note: ip.pn is IPv4-only. For IPv6 visitors,
mobiledefaults tofalseandproxy/hostingare derived from local MMDB flags (IP66 + Umkus).Note: ip.pn is called asynchronously with a 5-second timeout. If it fails or is unreachable, the app gracefully falls back to local MMDB flags.
-
License: Free (public)
-
URL:
https://www.dan.me.uk/torlist/?full -
Size: ~230 KB (~7,400 exit IPs)
-
Updates: Every 30 minutes (cached for 24 hours in-app to respect rate limits)
-
What we use:
- Full IP list → highest-priority Tor detection signal
- If
visitor_ipmatches, IP is classified astorbefore any other source is consulted
Tor detection priority: dan.me.uk exact match → IP66
is_tor_exit_node→ ip.pnproxyfallback. The first match wins and returnsip_type: "tor". dan.me.uk is checked first because it's the most comprehensive and up-to-date free Tor exit list available.Why 24-hour cache? dan.me.uk imposes a 30-minute rate limit per source IP — each VPS downloads fresh exit IPs once daily on the MMDB refresh cycle. Between updates, the cached list is loaded from disk (the Docker named volume persists it across restarts). If the download fails (rate-limited, network error), the app continues with the existing cache and retries on the next cycle.
| Field | Example | Source | Always present? |
|---|---|---|---|
ip |
203.177.x.x |
Request headers | ✅ Always |
ip_type |
residential |
Derived (dan.me.uk + IP66 + Umkus + ip.pn) | ✅ Always |
country_code |
PH |
GeoLite2 / IP66 | ✅ (if data available) |
country_name |
Philippines |
GeoLite2 / IP66 | ✅ |
continent_code |
AS |
GeoLite2 / IP66 | ✅ |
continent_name |
Asia |
GeoLite2 / IP66 | ✅ |
asn |
AS4775 |
IP66 | ✅ |
as_org |
Globe Telecoms |
IP66 / Umkus | ✅ |
rir |
apnic |
IP66 | ✅ |
city |
Taguig |
GeoLite2 City | Only for city-resolvable IPs |
latitude |
14.5185 |
GeoLite2 City | Only for city-resolvable IPs |
longitude |
121.0689 |
GeoLite2 City | Only for city-resolvable IPs |
timezone |
Asia/Manila |
GeoLite2 City | Only for city-resolvable IPs |
accuracy_radius |
10 |
GeoLite2 City | Only for city-resolvable IPs (km) |
postal_code |
1634 |
GeoLite2 City | Only if available |
region |
Metro Manila |
GeoLite2 City | Only if available |
registered_country |
DE |
GeoLite2 City | Always when GeoLite2 has registry data (null if missing) |
mobile |
true |
ip.pn / IP66 | ✅ Always (false for IPv6) |
proxy |
true |
ip.pn / IP66 | ✅ Always |
hosting |
true |
ip.pn / IP66 / Umkus | ✅ Always |
source |
ip66.dev (CC BY 4.0), ... |
Attribution | ✅ Always |
| Type | Meaning | Example | Derivation signals |
|---|---|---|---|
tor |
Tor exit node | 185.220.101.x |
dan.me.uk Tor exit list / IP66 is_tor_exit_node |
vpn |
Known VPN exit node | 209.50.171.x (Drei-K-Tech) |
IP66 is_anonymous_vpn |
proxy |
Open/public proxy | Various | IP66 is_public_proxy or ip.pn proxy=true |
datacenter |
Cloud/hosting provider | AWS, Google, Hetzner | Umkus hosting=1 or IP66 is_hosting_provider |
residential |
Home broadband / mobile ISP | Globe, PLDT, Comcast | Umkus hosting=0 + no proxy/vpn/tor flags |
unknown |
Could not classify | — | None of the above |
Derivation priority: tor (dan.me.uk) → tor (IP66) → vpn → proxy → datacenter → residential → unknown
These boolean flags are populated from multiple sources in priority order:
- dan.me.uk Tor exit list (IPv4 + IPv6) — if the IP matches,
proxyis forcibly set tofalsesince it's classified astor, not a proxy. - ip.pn API (IPv4 only) — returns all three directly. Most authoritative for IPv4 classification.
- IP66 anonymous_ip (IPv4 + IPv6) — derives
proxyfromis_tor_exit_node | is_anonymous_vpn | is_public_proxy,hostingfromis_hosting_provider - Umkus ip-index (IPv4 + IPv6) — derives
hostingfromhosting=1classification
For IPv6, mobile always defaults to false since ip.pn doesn't support IPv6.
{
"ip": "XXX.XXX.XXX.XXX",
"country_code": "PH",
"country_name": "Philippines",
"continent_code": "AS",
"continent_name": "Asia",
"asn": "AS4775",
"as_org": "Globe Telecoms",
"rir": "apnic",
"latitude": 14.5955,
"longitude": 120.9721,
"timezone": "Asia/Manila",
"accuracy_radius": 500,
"registered_country": "PH",
"mobile": true,
"proxy": false,
"hosting": false,
"ip_type": "residential",
"source": "ip66.dev (CC BY 4.0), MaxMind GeoLite2 (CC BY-SA 4.0), Umkus/ip-index (GPL 3.0), ip.pn (free API), dan.me.uk Tor exit list"
}{
"ip": "XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX",
"country_code": "PH",
"country_name": "Philippines",
"continent_code": "AS",
"continent_name": "Asia",
"asn": "AS4775",
"as_org": "Globe Telecoms",
"rir": "apnic",
"city": "Taguig",
"latitude": 14.5185,
"longitude": 121.0689,
"timezone": "Asia/Manila",
"accuracy_radius": 10,
"postal_code": "1634",
"region": "Metro Manila",
"mobile": false,
"proxy": false,
"hosting": false,
"ip_type": "residential",
"source": "ip66.dev (CC BY 4.0), MaxMind GeoLite2 (CC BY-SA 4.0), Umkus/ip-index (GPL 3.0), ip.pn (free API), dan.me.uk Tor exit list"
}{
"ip": "209.50.171.41",
"country_code": "US",
"country_name": "United States",
"continent_code": "NA",
"continent_name": "North America",
"asn": "AS200373",
"as_org": "Drei-K-Tech-GmbH 3xK Tech GmbH",
"rir": "ripencc",
"city": "Ashburn",
"latitude": 39.0469,
"longitude": -77.4903,
"timezone": "America/New_York",
"accuracy_radius": 20,
"postal_code": "20149",
"region": "Virginia",
"registered_country": "DE",
"mobile": false,
"proxy": true,
"hosting": false,
"ip_type": "vpn",
"source": "ip66.dev (CC BY 4.0), MaxMind GeoLite2 (CC BY-SA 4.0), Umkus/ip-index (GPL 3.0), ip.pn (free API), dan.me.uk Tor exit list"
}Notice:
country_codeisUS(physical location in Ashburn, Virginia) whileregistered_countryisDE(the AS is registered via RIPE in Germany). This is how VPN/proxy IPs look — the physical exit is in a different country from the registry.
{
"ip": "185.220.101.1",
"country_code": "DE",
"country_name": "Germany",
"continent_code": "EU",
"continent_name": "Europe",
"asn": "AS60729",
"as_org": "TORSERVERS-NET Stiftung Erneuerbare Freiheit",
"rir": "ripencc",
"city": "Brandenburg an der Havel",
"latitude": 52.6171,
"longitude": 13.1207,
"timezone": "Europe/Berlin",
"accuracy_radius": 20,
"postal_code": "14621",
"region": "Brandenburg",
"registered_country": "DE",
"mobile": false,
"proxy": false,
"hosting": false,
"ip_type": "tor",
"source": "ip66.dev (CC BY 4.0), MaxMind GeoLite2 (CC BY-SA 4.0), Umkus/ip-index (GPL 3.0), ip.pn (free API), dan.me.uk Tor exit list"
}{
"ip": "8.8.8.8",
"country_code": "US",
"country_name": "United States",
"continent_code": "NA",
"continent_name": "North America",
"asn": "AS15169",
"as_org": "Google LLC",
"rir": "arin",
"latitude": 37.751,
"longitude": -97.822,
"timezone": "America/Chicago",
"accuracy_radius": 1000,
"registered_country": "US",
"mobile": false,
"proxy": false,
"hosting": true,
"ip_type": "datacenter",
"source": "ip66.dev (CC BY 4.0), MaxMind GeoLite2 (CC BY-SA 4.0), Umkus/ip-index (GPL 3.0), ip.pn (free API), dan.me.uk Tor exit list"
}docker compose up -dDatabases auto-download on first start and cache in a Docker volume (~94 MB total). Startup takes ~5-10 seconds on first run (downloading DBs), instant on subsequent runs.
The app automatically reads CF-Connecting-IP (and falls back to X-Real-IP → X-Forwarded-For). No config needed. The real visitor IP is extracted before any lookup happens.
| Variable | Default | Description |
|---|---|---|
HOST |
0.0.0.0 |
Bind address |
PORT |
8081 |
HTTP port |
DB_DIR |
/data/db |
MMDB cache directory |
IPPN_API_BASE |
https://ip.pn/json/ |
ip.pn API base URL |
COUNTRY_DB_URL |
(empty) | Custom IP66/country DB URL |
CITY_DB_URL |
(see above) | Custom GeoLite2 city DB URL |
CLASSIFICATION_DB_URL |
(see above) | Custom Umkus classification DB URL |
TOR_EXIT_LIST_TTL |
86400 |
dan.me.uk cache TTL in seconds (24h default) |
LOG_LEVEL |
INFO |
Logging verbosity (DEBUG / INFO / WARNING) |
version: "3.8"
services:
ip-what:
build: .
ports:
- "8081:8081"
environment:
PORT: "8081"
COUNTRY_DB_URL: "https://downloads.ip66.dev/db/ip66.mmdb"
CITY_DB_URL: "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-City.mmdb"
CLASSIFICATION_DB_URL: "https://github.com/Umkus/ip-index/releases/download/latest/ip-index.mmdb"
volumes:
- ip-db-data:/data/db
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8081/health"]
interval: 60s
timeout: 5s
retries: 3
start_period: 30s
volumes:
ip-db-data:# Check local container health
curl http://localhost:8081/health
# Or via your deployed domain
curl https://your-domain.com/healthReturns:
{
"status": "ok",
"databases": {
"country_asn": true,
"city": true,
"classification": true,
"tor_exit_list": true
},
"loaded": ["country_asn", "city", "classification", "tor_exit_list"]
}ip-what/
├── app.py # FastAPI application — single GET / endpoint
├── Dockerfile # Container build
├── docker-compose.yml # Deployment config
├── requirements.txt # Python dependencies
├── .env.example # Environment variable reference
├── README.md # This file
├── utils/
│ ├── __init__.py
│ ├── downloader.py # Async MMDB downloader + dan.me.uk Tor exit list downloader
│ ├── geoip.py # GeoIP engine — maxminddb wrapper with multi-DB merge
│ └── test_downloader.py # 14-test suite covering all downloader edge cases
└── scripts/
└── batch_test.py # 92 IP batch test suite for regression testing
Before building this, I downloaded 12 MMDBs from 8 providers and probed ~227 IPs across the full IPv4 space to find the best combination. Here's how they compare:
| DB | Size | Coverage | Country | Name | Continent | ASN | ASN Org | City | Lat/Lon | TZ | Type | Proxy |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| IP66 🏆 | 17 MB | 98.7% | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
| xOS | 18 MB | 100% | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| GeoLite2 City | 64 MB | 97.4% | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
| Umkus ip-index | 13 MB | 83% | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ |
| DB-IP Lite | 47 MB | 92% | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
| IPLocate Country | 16 MB | 98.7% | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| HostByBelle Combo | 8.3 MB | 98.2% | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
Why this combination? No single free DB gives you everything. IP66 has the richest field set per byte (country + ASN + proxy flags + RIR in one 17 MB file) but lacks city data. GeoLite2 City adds city/lat/lon/timezone but is IPv4-only in most mirrors (we use P3TERX's combined v4+v6). Umkus adds the critical residential-vs-datacenter classification that nothing else provides for free. ip.pn fills in mobile/proxy/hosting booleans with zero setup cost.
| Method | Path | Description |
|---|---|---|
GET |
/ |
Your IP's geolocation + classification |
GET |
/health |
Database health status |
The app reads Cloudflare/proxy headers. You can simulate any IP locally with:
# Test a specific IP
curl -H "CF-Connecting-IP: 209.50.171.41" http://localhost:8081/
# Test IPv6
curl -H "CF-Connecting-IP: XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX" http://localhost:8081/
# Test your deployed instance (replace with your domain)
curl https://your-domain.com/This project uses data from multiple sources with specific attribution requirements:
- IP66 — Licensed under CC BY 4.0. Attribution:
ip66.dev (CC BY 4.0) - MaxMind GeoLite2 — Licensed under CC BY-SA 4.0. Attribution:
MaxMind GeoLite2 (CC BY-SA 4.0) - Umkus ip-index — Licensed under GPL 3.0. Attribution:
Umkus/ip-index (GPL 3.0) - ip.pn — Free API. Attribution:
ip.pn (free API) - dan.me.uk Tor exit list — Free public list. Attribution:
dan.me.uk Tor exit list
All attributions are automatically included in the source field of every API response.
The container may still have the old IPv4-only city DB cached. Nuke the volume to force re-download:
docker compose down
docker volume rm ip-what_ip-db-data
docker compose up -dCheck logs: docker logs ip-what.
That's correct — locally with curl http://localhost:8081/ there's no Cloudflare proxy, so the app sees the Docker bridge IP. Use the CF-Connecting-IP header to simulate:
curl -H "CF-Connecting-IP: 8.8.8.8" http://localhost:8081/This is expected for VPN/proxy IPs — see the registered_country field explanation above. The physical location (GeoLite2) may differ from the registry country (RIPE/ARIN/etc.).