Skip to content

gufranco/ip-vulture

Repository files navigation

ip-vulture

Share a link. Log their location. They see a dead server.



CI Node Fastify TypeScript Coverage License


2 runtime dependencies · 10 server disguises · 63 tests at 96% coverage · 492 lines of source · 0 data exposed to callers

Invisible Tracking

Every request resolves the caller's IP to country, city, ISP, and coordinates via ip-api.com. The geolocation appears only in your terminal logs. The caller sees nothing.

10 Server Disguises

Impersonate Apache, Nginx, IIS, Caddy, Lighttpd, LiteSpeed, Tomcat, OpenResty, Traefik, or HAProxy. Each template replicates real headers, content types, and HTML structure.

One-Command Tunnel

pnpm run local starts the server, opens an ngrok tunnel, and prints the public URL. Share it and watch the logs.

Rate-Limited and XSS-Safe

Built-in rate limiting stays under ip-api.com's free tier. All path-rendering templates escape HTML to prevent reflected XSS.

How It Works

sequenceDiagram
    participant C as Caller
    participant N as ngrok / Reverse Proxy
    participant S as ip-vulture
    participant G as ip-api.com

    C->>N: GET /any-path
    N->>S: Forward + X-Forwarded-For
    S->>G: GET /json/{caller-ip}
    G-->>S: Geolocation JSON
    S->>S: Log to terminal
    S-->>N: 404 Not Found
    N-->>C: Fake error page
Loading

The caller sees what looks like a misconfigured server returning a 404. You see their IP, country, city, ISP, coordinates, and timezone in the terminal. If ip-api.com is down, the disguise holds: the caller still gets the fake 404 page.

Server Templates

Pick a disguise with the SERVER_TEMPLATE environment variable. Set it to random to let ip-vulture pick one at startup.

Template Server Header Content-Type Path in Body
apache Apache/2.4.62 (Ubuntu) text/html; charset=iso-8859-1 Yes
nginx nginx/1.27.4 text/html No
iis Microsoft-IIS/10.0 text/html No
caddy Caddy none No
lighttpd lighttpd/1.4.76 text/html No
litespeed LiteSpeed text/html No
tomcat none text/html;charset=utf-8 Yes
openresty openresty/1.27.1.1 text/html No
traefik none text/plain; charset=utf-8 No
haproxy none text/html No

IIS also sets X-Powered-By: ASP.NET. Traefik sets X-Content-Type-Options: nosniff. HAProxy sets Cache-Control: no-cache. Every header matches the real server's default behavior.

Quick Start

Prerequisites

Tool Version Install
Node.js >= 24 nodejs.org
pnpm >= 9 corepack enable pnpm
ngrok any ngrok.com

ngrok is only needed for the tunnel mode. Direct server hosting requires no additional tools.

Setup

git clone https://github.com/gufranco/ip-vulture.git
cd ip-vulture
pnpm install
cp .env.example .env

Run with ngrok

pnpm run local
========================================
  https://xxxx-xx-xx-xx-xx.ngrok-free.app
========================================

Share the URL. Append any path to it. Watch the terminal.

Run on a server

pnpm start

Set HOST and PORT in .env to match your deployment. Works behind any reverse proxy that sets X-Forwarded-For.

Verify

curl http://localhost:3000/health
# {"status":"ok"}

What the caller sees

Depends on the template. With apache (the default):

Not Found

The requested URL /any-path was not found on this server.

Apache/2.4.62 (Ubuntu) Server at localhost Port 80

Headers match a real Apache server: Content-Type: text/html; charset=iso-8859-1 and Server: Apache/2.4.62 (Ubuntu).

What you see

{
  "id": "any-path",
  "ip": "203.0.113.50",
  "geo": {
    "country": "United States",
    "city": "New York",
    "isp": "Verizon",
    "lat": 40.7128,
    "lon": -74.006
  },
  "msg": "geolocation resolved"
}

Configuration

Variable Default Description
PORT 3000 Server port. Validated at startup: must be 1-65535
HOST 0.0.0.0 Bind address. Use 0.0.0.0 for ngrok or container deployments
SERVER_TEMPLATE apache Which server to impersonate. One of: apache, nginx, iis, caddy, lighttpd, litespeed, tomcat, openresty, traefik, haproxy, random

Scripts

Command Description
pnpm run local Start server + ngrok, print public URL, stream logs
pnpm dev Start server with auto-reload, no tunnel
pnpm start Start server in production mode
pnpm test Run test suite
pnpm test -- --coverage Run tests with coverage report
pnpm run lint Check formatting and lint rules
pnpm run lint:fix Auto-fix formatting and lint issues
pnpm run typecheck Run TypeScript type checker
Project structure
src/
  app.ts              # Fastify app factory with trustProxy and rate limiting
  config.ts           # Env var validation and typed config loader
  server.ts           # Entry point, graceful shutdown
  routes/
    health.ts         # GET /health liveness probe (rate-limit exempt)
    locate.ts         # GET / and GET /:id with geolocation + fake 404
  templates/
    template.ts       # ServerTemplate interface and ServerName enum
    registry.ts       # Template registry, resolver, and random picker
    escape.ts         # Shared HTML escaping for XSS prevention
    apache.ts         # Apache 2.4.62 (Ubuntu) 404 page
    nginx.ts          # nginx 1.27.4 404 page
    iis.ts            # Microsoft-IIS/10.0 404 page
    caddy.ts          # Caddy empty response
    lighttpd.ts       # lighttpd 1.4.76 404 page
    litespeed.ts      # LiteSpeed styled 404 page
    tomcat.ts         # Apache Tomcat 10.1.34 404 page (XSS-safe)
    openresty.ts      # openresty 1.27.1.1 404 page
    traefik.ts        # Traefik plain-text 404
    haproxy.ts        # HAProxy 404 page
  __tests__/
    config.test.ts    # Config validation tests
    escape.test.ts    # HTML escaping unit tests
    locate.test.ts    # Integration tests for locate and health routes
    registry.test.ts  # Unit tests for template resolution
    templates.test.ts # Contract tests for all 10 templates
scripts/
  local.sh            # Orchestrates server + ngrok with cleanup trap
FAQ
Why does ip-api.com show a VPN location instead of the real one?

ip-api.com resolves the exit IP. If the caller uses a VPN, you see the VPN server's location. There is no way around this at the network level.

Why HTTP for ip-api.com instead of HTTPS?

The free tier of ip-api.com only supports HTTP. The call happens server-side, so it never touches the caller's browser. Paid plans support HTTPS.

What is the rate limit?

ip-api.com allows 45 requests per minute on the free tier. ip-vulture enforces a server-side limit of 40 requests per minute to stay safely under this threshold. The /health endpoint is exempt from rate limiting.

What happens when ip-api.com is down or rate-limited?

The caller still sees the fake 404 page. The geolocation lookup fails silently and logs a warning to your terminal. A 5-second AbortSignal.timeout prevents the request from hanging indefinitely. The disguise is never broken.

Can I host this on a server without ngrok?

Yes. Run pnpm start with HOST and PORT set in .env. The server works behind any reverse proxy that sets X-Forwarded-For. The trustProxy setting extracts the real client IP automatically.

How do I add a new server template?

Create a new file in src/templates/ implementing the ServerTemplate interface: a name from the ServerName enum, a frozen headers object, and a render(path) function. If the template renders the path in its body, use escapeHtml() from escape.ts to prevent XSS. Add the enum value to ServerName in template.ts and register it in the templates map in registry.ts.

License

MIT

About

Share a link, log their location, they see a dead server. Fastify + TypeScript with 10 server disguises, IP geolocation via ip-api.com, rate limiting, XSS-safe templates, and 63 tests at 96% coverage

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors