2 runtime dependencies · 10 server disguises · 63 tests at 96% coverage · 492 lines of source · 0 data exposed to callers
|
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. |
Impersonate Apache, Nginx, IIS, Caddy, Lighttpd, LiteSpeed, Tomcat, OpenResty, Traefik, or HAProxy. Each template replicates real headers, content types, and HTML structure. |
|
|
Built-in rate limiting stays under ip-api.com's free tier. All path-rendering templates escape HTML to prevent reflected XSS. |
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
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.
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.
| 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.
git clone https://github.com/gufranco/ip-vulture.git
cd ip-vulture
pnpm install
cp .env.example .envpnpm run local========================================
https://xxxx-xx-xx-xx-xx.ngrok-free.app
========================================
Share the URL. Append any path to it. Watch the terminal.
pnpm startSet HOST and PORT in .env to match your deployment. Works behind any reverse proxy that sets X-Forwarded-For.
curl http://localhost:3000/health
# {"status":"ok"}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).
{
"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"
}| 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 |
| 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.