Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ip-what

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/

How it works

  1. You visit the site
  2. App detects your real public IP from Cloudflare headers (CF-Connecting-IP / X-Real-IP / X-Forwarded-For)
  3. Looks it up in three local MMDB databases (auto-downloaded on first start, cached in a Docker volume)
  4. Enriches with ip.pn API for IPv4 proxy/mobile/hosting flags (no rate limit, no auth)
  5. Checks dan.me.uk Tor exit list for fastest and most comprehensive Tor node detection
  6. Derives IP type from combined signals across all sources (dan.me.uk → IP66 → Umkus → ip.pn priority)
  7. Returns JSON with geolocation + classification

IPv4 and IPv6 both supported — the app detects whichever IP version you connect on.


Data Sources

We use five data sources, each contributing different fields. Here's exactly what we use from each:

1. IP66 (ip66.dev)

  • License: CC BY 4.0
  • URL: https://downloads.ip66.dev/db/ip66.mmdb
  • Size: ~17 MB
  • Updates: Daily
  • What we use:
    • country.iso_codecountry_code
    • country.names.encountry_name
    • continent.codecontinent_code
    • continent.names.encontinent_name
    • autonomous_system_numberasn (formatted as AS{n})
    • autonomous_system_organizationas_org
    • rirrir (Regional Internet Registry — ARIN/RIPE/APNIC/etc.)
    • anonymous_ip.is_tor_exit_node — used in IP type derivation
    • anonymous_ip.is_anonymous_vpn — used in IP type derivation
    • anonymous_ip.is_public_proxy — used in IP type + proxy flag
    • anonymous_ip.is_hosting_provider — used in IP type + hosting flag

2. GeoLite2 City (P3TERX mirror)

  • 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.encity
    • location.latitudelatitude
    • location.longitudelongitude
    • location.time_zonetimezone
    • location.accuracy_radiusaccuracy_radius (in km)
    • postal.codepostal_code
    • subdivisions[].names.enregion (state/province)
    • country.iso_codecountry_code (takes priority over IP66 for physical location accuracy)
    • country.names.encountry_name
    • continent.codecontinent_code
    • continent.names.encontinent_name
    • registered_country.iso_coderegistered_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_country field.

    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.

3. Umkus ip-index

  • 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 + hosting flag
    • asn_name — backfill for as_org if IP66 has no org name

4. ip.pn API (enrichment)

  • License: Free API (no auth, no rate limit)

  • URL: https://ip.pn/json/{IP}

  • What we use:

    • mobilemobile boolean
    • proxyproxy boolean
    • hostinghosting boolean

    Note: ip.pn is IPv4-only. For IPv6 visitors, mobile defaults to false and proxy/hosting are 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.

5. dan.me.uk Tor Exit List

  • 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_ip matches, IP is classified as tor before any other source is consulted

    Tor detection priority: dan.me.uk exact match → IP66 is_tor_exit_node → ip.pn proxy fallback. The first match wins and returns ip_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.


Response Fields

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

ip_type Values

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

mobile, proxy, hosting Derivation

These boolean flags are populated from multiple sources in priority order:

  1. dan.me.uk Tor exit list (IPv4 + IPv6) — if the IP matches, proxy is forcibly set to false since it's classified as tor, not a proxy.
  2. ip.pn API (IPv4 only) — returns all three directly. Most authoritative for IPv4 classification.
  3. IP66 anonymous_ip (IPv4 + IPv6) — derives proxy from is_tor_exit_node | is_anonymous_vpn | is_public_proxy, hosting from is_hosting_provider
  4. Umkus ip-index (IPv4 + IPv6) — derives hosting from hosting=1 classification

For IPv6, mobile always defaults to false since ip.pn doesn't support IPv6.


Example Responses

Residential (Globe Telecom, Philippines — IPv4 via ip.pn)

{
  "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"
}

Residential (Globe Telecom, Philippines — IPv6 via local DB fallback)

{
  "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"
}

VPN / Proxy (Ashburn exit, German AS — shows registered_country)

{
  "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_code is US (physical location in Ashburn, Virginia) while registered_country is DE (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.

Tor Exit Node

{
  "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"
}

Datacenter (Google DNS)

{
  "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"
}

Deploy

Quick start

docker compose up -d

Databases 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.

Behind Cloudflare

The app automatically reads CF-Connecting-IP (and falls back to X-Real-IPX-Forwarded-For). No config needed. The real visitor IP is extracted before any lookup happens.

Configuration via environment variables

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)

Docker Compose

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:

Health Check

# Check local container health
curl http://localhost:8081/health

# Or via your deployed domain
curl https://your-domain.com/health

Returns:

{
  "status": "ok",
  "databases": {
    "country_asn": true,
    "city": true,
    "classification": true,
    "tor_exit_list": true
  },
  "loaded": ["country_asn", "city", "classification", "tor_exit_list"]
}

Project Structure

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

Database Source Comparison

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.


API Endpoints

Method Path Description
GET / Your IP's geolocation + classification
GET /health Database health status

Simulating IPs for testing

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/

Attribution Requirements

This project uses data from multiple sources with specific attribution requirements:

  1. IP66 — Licensed under CC BY 4.0. Attribution: ip66.dev (CC BY 4.0)
  2. MaxMind GeoLite2 — Licensed under CC BY-SA 4.0. Attribution: MaxMind GeoLite2 (CC BY-SA 4.0)
  3. Umkus ip-index — Licensed under GPL 3.0. Attribution: Umkus/ip-index (GPL 3.0)
  4. ip.pn — Free API. Attribution: ip.pn (free API)
  5. 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.


Troubleshooting

"No city data"

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 -d

"Container not healthy after 60 seconds"

Check logs: docker logs ip-what.

"I see private Docker IP (172.x.x.x)"

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/

"Country + city don't match (e.g., DE country with Ashburn city)"

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.).

About

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).

Resources

Stars

Watchers

Forks

Contributors

Languages