echowhat the internet sees when you connect

Documentation

echo shows you exactly what the internet sees when you connect: your IP address, location, ISP and more. This page covers general usage, the public API, and running it yourself.

Introduction

echo is a server-side IP and geo lookup tool. When you open the site it detects your public IP, looks it up against a bundled geo database, and shows location, country, coordinates, ISP/ASN, timezone and hostname. You can also look up any IP address, resolve DNS records, and query WHOIS registration data — all without a client-side geo call.

What echo does

  • Shows your public IP and geo information from a bundled database
  • Looks up any IP via ?ip= on the home page or the API
  • Resolves forward DNS records (A, AAAA, CNAME, MX, NS, TXT, SOA, SRV, CAA)
  • Queries WHOIS/RDAP registration and ASN data on demand
  • Runs an optional IPv4/IPv6 connectivity diagnostic from your browser
  • Logs aggregate lookup statistics — never raw visitor IPs publicly

Lookup any IP

Add ?ip=8.8.8.8 to the home page URL to look up any public IP:

https://echo.johansen.foo/?ip=8.8.8.8

Both IPv4 and IPv6 are supported. The page shows the lookup result instead of your own details and the copy and share actions work for the looked-up address.

Share links

Use the copy link button to get a shareable ?ip= URL. It uses the configured APP_URL when set, otherwise the address you are viewing from. Lookup pages set noindex metadata so search engines do not index query-specific URLs.

Map and copy

The coordinates card opens a Leaflet map centred on the approximate city-level location. Copy and copy-as-JSON buttons put the IP or the full lookup payload on your clipboard. Clipboard failures are reported honestly.

Connectivity diagnostics

When CONNECTIVITY_IPV4_URL or CONNECTIVITY_IPV6_URL are configured, a connectivity section lets your browser probe each endpoint and report IPv4/IPv6 reachability with latency. This measures browser reachability only — it never changes the IP recorded by the server. The section is hidden when no probe URL is configured.

Privacy model

The public site never exposes raw visitor IPs. Recent lookups are shown as aggregate totals and top countries. Exact IPs are visible only in the token-protected admin dashboard. Lookup rows are pruned after LOOKUP_RETENTION_DAYS (default 90 days). No cookies are used except the optional admin session cookie.

Overview

All public endpoints are GET. JSON endpoints return Access-Control-Allow-Origin: * and are rate limited per visitor IP — /api/ip returns plain text without CORS, and /api/stats and /api/health omit CORS (health is unthrottled). The visitor IP is read from X-Real-IP first, then X-Forwarded-For; your reverse proxy must overwrite these headers with the verified client address.

JSON errors use a stable shape:

{ "error": "human readable message", "code": "stable_code" }

Rate limiting and errors

Each public endpoint has its own fixed-window budget (defaults below). A response carries x-ratelimit-limit and x-ratelimit-remaining. When a budget is exhausted the API returns HTTP 429 with a retry-after header in seconds.

EndpointDefault budget (per 60s)
/api/ip60
/api/json30
/api/history30
/api/whois10
/api/dns10

GET /api/ip

Returns the visitor IP as plain text, or a specific IP when ?ip= is provided.

curl https://echo.johansen.foo/api/ip
203.0.113.7
curl "https://echo.johansen.foo/api/ip?ip=8.8.8.8"
8.8.8.8

GET /api/json

Returns the full normalized lookup payload for the visitor or for ?ip=.

curl "https://echo.johansen.foo/api/json?ip=8.8.8.8"
{
  "ip": "8.8.8.8",
  "city": "Mountain View",
  "region": "California",
  "country": "US",
  "countryName": "United States",
  "flag": "🇺🇸",
  "org": "Google LLC",
  "asn": "AS15169",
  "timezone": "America/Los_Angeles",
  "utcOffset": "-07:00",
  "latitude": 37.422,
  "longitude": -122.085,
  "hostname": null,
  "isPrivate": false
}

GET /api/whois

Returns WHOIS/RDAP registration and ASN data for ?ip= (on demand, cached).

curl "https://echo.johansen.foo/api/whois?ip=8.8.8.8"
{
  "ip": {
    "handle": "NET-8-8-8-0-2",
    "name": "GOGL",
    "startAddress": "8.8.8.0",
    "endAddress": "8.8.8.255",
    "country": "US",
    "cidr": "8.8.8.0/24",
    "organization": "Google LLC",
    "registrant": "Google LLC",
    "abuse": null
  },
  "asn": {
    "handle": "AS15169",
    "name": "GOOGLE",
    "country": "US",
    "organization": "Google LLC"
  }
}

GET /api/dns

Resolves A, AAAA, CNAME, MX, NS, TXT, SOA, SRV and CAA records for ?name= with cache metadata.

curl "https://echo.johansen.foo/api/dns?name=example.com"
{
  "name": "example.com",
  "records": {
    "a": ["93.184.216.34"],
    "aaaa": [],
    "mx": [],
    "ns": ["a.iana-servers.net"],
    "txt": [],
    "soa": ["a.iana-servers.net hostmaster.iana.org"]
  },
  "cache": "miss",
  "resolvedAt": "2026-08-20T10:00:00.000Z",
  "durationMs": 42,
  "partial": false
}

GET /api/history

Returns aggregate lookup statistics: totals and top countries, never raw IPs.

curl https://echo.johansen.foo/api/history
{
  "total": 1234,
  "last24h": 56,
  "topCountries": [
    { "iso": "US", "count": 42 },
    { "iso": "DE", "count": 7 }
  ]
}

GET /api/stats

Private owner analytics guarded by STATS_TOKEN. Pass ?token= or an Authorization: Bearer header.

curl -H "Authorization: Bearer $STATS_TOKEN" https://echo.johansen.foo/api/stats
{
  "total": 1234,
  "last24h": 56,
  "topCountries": [],
  "topIps": [{ "ip": "203.0.113.7", "count": 12 }],
  "daily": [{ "day": "2026-08-20", "count": 56 }]
}

GET /api/health

Public liveness check returning {status:ok}. Readiness detail requires the HEALTH_TOKEN.

curl https://echo.johansen.foo/api/health
{ "status": "ok" }

Environment variables

VariableDefaultDescription
APP_URLhttps://echo.johansen.fooPublic origin used for metadata, sitemap and share links
TZEurope/CopenhagenContainer timezone for logs and admin timestamps
RATE_LIMIT_MAX30Legacy global fallback for request budget per window
RATE_LIMIT_WINDOW_MS60000Legacy global fallback window
RATE_LIMIT_<ENDPOINT>_MAXip 60, json 30, history 30, whois 10, dns 10, stats-auth 5Per-endpoint budget
RATE_LIMIT_<ENDPOINT>_WINDOW_MS60000Per-endpoint window
DNS_TIMEOUT_MS6000Overall DNS resolution deadline
DNS_MAX_CONCURRENCY2Concurrent resolver jobs
DNS_CACHE_TTL_MS30000Successful DNS cache lifetime
DNS_FAILURE_TTL_MS5000Failed DNS cache lifetime
DNS_CACHE_MAX100Maximum cached hostnames
LOOKUP_RETENTION_DAYS90How long lookup rows are kept
STATS_TOKEN(unset)Secret protecting /api/stats; endpoint disabled when unset
HEALTH_TOKEN(unset)Secret protecting authenticated readiness detail
ADMIN_TOKEN(unset)Secret enabling the /admin dashboard; hidden when unset
ADMIN_SESSION_TTL_SECONDS28800Admin session lifetime in seconds
CONNECTIVITY_IPV4_URL(unset)IPv4 probe endpoint used by the browser diagnostic
CONNECTIVITY_IPV6_URL(unset)IPv6 probe endpoint used by the browser diagnostic
ECHO_IMAGE_SIZE_BYTES(unset)Optional deployed image size shown in admin resources
UMAMI_SCRIPT_URLhttps://umami.johansen.foo/script.jsUmami script URL; analytics only when set
UMAMI_WEBSITE_ID(unset)Umami website id; analytics only when set
CARTO_BASEMAP_KEY(unset)API key for CARTO basemap tiles; avoids the "API key required" watermark

Deployment

Images are published to GitHub Container Registry. Copy docker-compose.yml to your host, configure a .env next to it, then:

docker compose pull
docker compose up -d

The service listens on port 3100. Keep the host firewall closed to direct access if a reverse proxy is the only intended entry point; TLS and HSTS are owned by the external TLS proxy. Data persists in the echo-data volume.

Nginx Proxy Manager

NPM 2.x Proxy Hosts automatically overwrite X-Real-IP, X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host, so a single-hop setup needs no manual configuration. echo trusts X-Real-IP first.

With Cloudflare in front, open the Advanced tab and add: set_real_ip_from for each Cloudflare range, then real_ip_header CF-Connecting-IP;. For an intermediate proxy, use set_real_ip_from with that proxy's range and real_ip_header X-Forwarded-For; with real_ip_recursive on;.

Connectivity probes

The connectivity diagnostic needs two lightweight endpoints reachable from browsers: one over IPv4 only and one over IPv6 only. Configure CONNECTIVITY_IPV4_URL and CONNECTIVITY_IPV6_URL with those URLs. The probe responses should be CORS-enabled and return a minimal payload; they are never used for lookups or logged as visitor activity.

Private admin dashboard

Set ADMIN_TOKEN to enable /admin. The dashboard shows exact visitor activity (type, channel, actor, country, IP, target), aggregate breakdowns, and resource sampling (CPU, memory, storage). Sessions are signed HttpOnly cookies; logout revokes the session server-side. When ADMIN_TOKEN is unset the whole area returns 404.

Releasing

Releases use npm run release:patch, npm run release:minor or npm run release:major. The script bumps the version, moves the Unreleased changelog into a dated section, commits and tags. Pushing the tag triggers CI to build and publish the image to GitHub Container Registry.

Data and attribution

Geo data comes from db-ip and is licensed under CC BY 4.0. Attribution is shown in the site footer.