basic proxy ip detection test tool

This commit is contained in:
Bojan Kucera 2025-06-05 21:14:29 -04:00
commit d3cff15545
15 changed files with 1075 additions and 0 deletions

115
.gitignore vendored Normal file
View file

@ -0,0 +1,115 @@
# Dependencies
node_modules/
bun.lockb
# Build outputs
dist/
build/
*.tsbuildinfo
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Dependency directories
jspm_packages/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Storybook build outputs
.out
.storybook-out
# Temporary folders
tmp/
temp/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Docker
.dockerignore
# Kubernetes secrets (keep template)
k8s-secret-production.yaml
# Local development
*.local

42
.gitlab-ci-simple.yml Normal file
View file

@ -0,0 +1,42 @@
# Simple GitLab CI/CD Pipeline - Build and Deploy only
stages:
- build
- deploy
variables:
DOCKER_DRIVER: overlay2
IMAGE_NAME: $CI_REGISTRY_IMAGE
IMAGE_TAG: $CI_COMMIT_SHA
# Build and push Docker image
build:
stage: build
image: docker:24.0.5
services:
- docker:24.0.5-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- docker info
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker pull $CI_REGISTRY_IMAGE:latest || true
- docker build --cache-from $CI_REGISTRY_IMAGE:latest --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --tag $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
only:
- master
# Deploy to production
deploy:
stage: deploy
image: ictu/sshpass
script:
- sshpass -p c4c5f47918a6310d79de188465218b440c69570c ssh -o StrictHostKeyChecking=no kubeuser@stare.gg 'kubectl rollout restart deployments/stare-site -n stare'
environment:
name: production
only:
- master

32
Dockerfile Normal file
View file

@ -0,0 +1,32 @@
# Use the official Bun image as base - much smaller than Node.js
FROM oven/bun:1-slim as base
# Set working directory
WORKDIR /app
# Copy package files
COPY package.json bun.lockb* ./
# Install dependencies
RUN bun install --frozen-lockfile --production
# Copy source code
COPY src/ ./src/
# Expose the port
EXPOSE 9999
# Create non-root user for security
RUN addgroup --system --gid 1001 bunuser && \
adduser --system --uid 1001 bunuser
# Change ownership of the app directory
RUN chown -R bunuser:bunuser /app
USER bunuser
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD bun --version || exit 1
# Start the application
CMD ["bun", "run", "start"]

81
KUBERNETES.md Normal file
View file

@ -0,0 +1,81 @@
# Kubernetes Deployment
## Quick Start
### Prerequisites
- Kubernetes cluster running
- `kubectl` configured to access your cluster
- Docker registry access (for GitLab CI)
### Deploy to Kubernetes
**Option 1: Use deployment script**
```bash
# Linux/Mac
chmod +x deploy.sh
./deploy.sh
# Windows
deploy.bat
```
**Option 2: Manual deployment**
```bash
# 1. Create secret for API key
kubectl apply -f k8s-secret.yaml
# 2. Deploy application
kubectl apply -f k8s-deployment.yaml
# 3. Optional: Deploy ingress for external access
kubectl apply -f k8s-ingress.yaml
```
### Test the deployment
```bash
# Port forward to test locally
kubectl port-forward service/proxy-detection-service 8080:80
# Test health endpoint (no auth required)
curl http://localhost:8080/health
# Test main endpoint (requires API key)
curl -H "X-API-Key: bd406bf53ddc6abe1d9de5907830a955" http://localhost:8080/
```
### Configuration
#### Update API Key
1. Generate base64 encoded API key:
```bash
echo -n "your-new-api-key" | base64
```
2. Update `k8s-secret.yaml` with the new value
3. Apply changes:
```bash
kubectl apply -f k8s-secret.yaml
kubectl rollout restart deployment/proxy-detection-api
```
#### Update Image
Update image in `k8s-deployment.yaml` or use kubectl:
```bash
kubectl set image deployment/proxy-detection-api proxy-detection-api=your-registry/proxy-detection-api:new-tag
```
### GitLab CI/CD
The `.gitlab-ci-simple.yml` provides:
- **Build stage**: Builds and pushes Docker image to GitLab registry
- **Deploy stage**: Updates Kubernetes deployment (manual trigger)
Replace `.gitlab-ci.yml` with `.gitlab-ci-simple.yml` for the minimal pipeline.
### Resources
The deployment is configured with minimal resources:
- **Requests**: 64Mi RAM, 50m CPU
- **Limits**: 128Mi RAM, 100m CPU
- **Replicas**: 2 (for basic high availability)
Adjust these values in `k8s-deployment.yaml` based on your needs.

221
README.md Normal file
View file

@ -0,0 +1,221 @@
# Proxy Detection API
A simple Bun and Fastify API that extracts IP addresses from HTTP request headers to detect potential proxy leaks.
## Features
- **Built-in IP Detection**: Fastify's `trustProxy` automatically parses proxy headers
- **Full IP Chain**: Get the complete proxy chain, not just the client IP
- **Custom Header Analysis**: Extracts IPs from 17+ different proxy headers
- **No External Dependencies**: Works entirely by analyzing request headers
- **Fast Performance**: Fastify is one of the fastest Node.js frameworks
## Installation
```bash
bun install
```
## Usage
### Development
```bash
bun run dev
```
### Production
```bash
bun run start
```
## Docker Deployment
### Quick Start with Docker
```bash
# Build the image
npm run docker:build
# Run the container
npm run docker:run
```
### Using Docker Compose
```bash
# Development
npm run docker:dev
# Production (with nginx reverse proxy)
npm run docker:prod
```
### Manual Docker Commands
```bash
# Build
docker build -t proxy-detection-api .
# Run
docker run -p 9999:9999 proxy-detection-api
# Run with environment variables
docker run -p 9999:9999 -e NODE_ENV=production proxy-detection-api
```
## GitLab CI/CD
This project includes a complete GitLab CI/CD pipeline that:
1. **Tests** - Runs tests and TypeScript compilation
2. **Builds** - Creates Docker images and pushes to GitLab Container Registry
3. **Deploys** - Deploys to staging/production environments
### Pipeline Stages:
- `test` - Runs on MRs and main branch
- `build` - Builds Docker images on main/develop/tags
- `deploy_staging` - Manual deployment to staging (develop branch)
- `deploy_production` - Manual deployment to production (main branch/tags)
### Required GitLab Variables:
- `CI_REGISTRY_USER` - GitLab registry username (auto-provided)
- `CI_REGISTRY_PASSWORD` - GitLab registry password (auto-provided)
- Add your deployment-specific variables in GitLab CI/CD settings
The API will be available at `http://localhost:9999`
### Endpoints
#### `GET /`
Returns API information and available endpoints.
#### `GET /detect`
Extracts all IP addresses found in request headers with full proxy chain detection.
**Response:**
```json
{
"success": true,
"clientIP": "203.0.113.45", // Fastify's auto-detected client IP
"ipChain": ["203.0.113.45", "192.168.1.100"], // Complete proxy chain
"foundIPs": ["192.168.1.100", "203.0.113.45"], // IPs found by custom parser
"totalFound": 2,
"headersSources": {
"x-forwarded-for": ["192.168.1.100", "203.0.113.45"],
"x-real-ip": ["203.0.113.45"]
},
"allHeaders": { ... },
"timestamp": 1672531200000
}
```
## What's New with Fastify
### Trust Proxy Support
Fastify automatically handles proxy headers when `trustProxy: true` is enabled:
- Parses `X-Forwarded-For` header automatically
- Provides `request.ip` (client IP) and `request.ips` (full chain)
- More secure than manual header parsing
### Built-in IP Chain Detection
```javascript
// Fastify automatically provides:
request.ip // "203.0.113.45" (real client)
request.ips // ["203.0.113.45", "10.0.0.1", "192.168.1.100"] (full chain)
```
## Installation
```bash
# Install dependencies
bun install
```
## Usage
```bash
# Development (with auto-reload)
bun run dev
# Production
bun run start
```
## API Endpoints
### `GET /`
Returns API information and available endpoints.
### `GET /detect`
Performs a comprehensive proxy detection check:
```json
{
"success": true,
"data": {
"yourIP": "1.2.3.4",
"externalIPs": [
{
"ip": "1.2.3.4",
"source": "ipify",
"timestamp": 1234567890
}
],
"isProxyWorking": true,
"leakedIPs": [],
"timestamp": 1234567890
},
"message": "Proxy is working correctly"
}
```
### `GET /quick`
Quick IP check using a single service:
```json
{
"ip": "1.2.3.4",
"timestamp": 1234567890
}
```
### `GET /headers`
Shows request headers and client IP information:
```json
{
"clientIP": "1.2.3.4",
"headers": {
"x-forwarded-for": "1.2.3.4",
"user-agent": "Mozilla/5.0..."
}
}
```
## How It Works
1. **Multiple IP Sources**: Queries several IP detection services (ipify, httpbin, ipapi, jsonip)
2. **Comparison**: Compares results to detect inconsistencies
3. **Leak Detection**: If different services return different IPs, it indicates a potential proxy leak
4. **Analysis**: Determines if your proxy is working correctly
## Use Cases
- Test if your VPN/proxy is working
- Detect DNS leaks
- Monitor IP consistency across services
- Verify anonymity tools
## Example Response (Proxy Working)
```json
{
"yourIP": "192.168.1.100",
"isProxyWorking": true,
"leakedIPs": [],
"message": "Proxy is working correctly"
}
```
## Example Response (IP Leak Detected)
```json
{
"yourIP": "192.168.1.100",
"isProxyWorking": false,
"leakedIPs": ["203.0.113.5"],
"message": "Potential IP leak detected: 203.0.113.5"
}
```

126
bun.lock Normal file
View file

@ -0,0 +1,126 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "proxy-detection-api",
"dependencies": {
"@fastify/cors": "^11.0.1",
"fastify": "^5.3.3",
},
"devDependencies": {
"@types/bun": "latest",
"@types/node": "^22.15.30",
"typescript": "^5.8.3",
},
},
},
"packages": {
"@fastify/ajv-compiler": ["@fastify/ajv-compiler@4.0.2", "", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0" } }, "sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ=="],
"@fastify/cors": ["@fastify/cors@11.0.1", "", { "dependencies": { "fastify-plugin": "^5.0.0", "toad-cache": "^3.7.0" } }, "sha512-dmZaE7M1f4SM8ZZuk5RhSsDJ+ezTgI7v3HHRj8Ow9CneczsPLZV6+2j2uwdaSLn8zhTv6QV0F4ZRcqdalGx1pQ=="],
"@fastify/error": ["@fastify/error@4.2.0", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="],
"@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.0.3", "", { "dependencies": { "fast-json-stringify": "^6.0.0" } }, "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ=="],
"@fastify/forwarded": ["@fastify/forwarded@3.0.0", "", {}, "sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA=="],
"@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="],
"@fastify/proxy-addr": ["@fastify/proxy-addr@5.0.0", "", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA=="],
"@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="],
"@types/node": ["@types/node@22.15.30", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA=="],
"abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="],
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
"avvio": ["avvio@9.1.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw=="],
"bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="],
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-json-stringify": ["fast-json-stringify@6.0.1", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^2.0.0", "rfdc": "^1.2.0" } }, "sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg=="],
"fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="],
"fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="],
"fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="],
"fastify": ["fastify@5.3.3", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.0", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^9.0.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-nCBiBCw9q6jPx+JJNVgO8JVnTXeUyrGcyTKPQikRkA/PanrFcOIo4R+ZnLeOLPZPGgzjomqfVarzE0kYx7qWiQ=="],
"fastify-plugin": ["fastify-plugin@5.0.1", "", {}, "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ=="],
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
"find-my-way": ["find-my-way@9.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg=="],
"ipaddr.js": ["ipaddr.js@2.2.0", "", {}, "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA=="],
"json-schema-ref-resolver": ["json-schema-ref-resolver@2.0.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"light-my-request": ["light-my-request@6.6.0", "", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="],
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
"pino": ["pino@9.7.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg=="],
"pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="],
"pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="],
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"safe-regex2": ["safe-regex2@5.0.0", "", { "dependencies": { "ret": "~0.5.0" } }, "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
"secure-json-parse": ["secure-json-parse@4.0.0", "", {}, "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
"sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="],
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="],
"toad-cache": ["toad-cache@3.7.0", "", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
}
}

32
deploy.bat Normal file
View file

@ -0,0 +1,32 @@
@echo off
echo 🚀 Deploying Proxy Detection API to Kubernetes...
REM Apply secret (API key)
echo 📋 Creating secret...
kubectl apply -f k8s-secret.yaml
REM Apply deployment and service
echo 🔧 Deploying application...
kubectl apply -f k8s-deployment.yaml
REM Optional: Apply ingress
set /p deploy_ingress="Do you want to deploy ingress? (y/N): "
if /i "%deploy_ingress%"=="y" (
echo 🌐 Deploying ingress...
kubectl apply -f k8s-ingress.yaml
)
REM Check deployment status
echo 📊 Checking deployment status...
kubectl rollout status deployment/proxy-detection-api
REM Show service info
echo 📋 Service information:
kubectl get service proxy-detection-service
echo ✅ Deployment completed!
echo 💡 To test the API:
echo kubectl port-forward service/proxy-detection-service 8080:80
echo curl http://localhost:8080/health
pause

33
deploy.sh Normal file
View file

@ -0,0 +1,33 @@
#!/bin/bash
# Simple Kubernetes deployment script
echo "🚀 Deploying Proxy Detection API to Kubernetes..."
# Apply secret (API key)
echo "📋 Creating secret..."
kubectl apply -f k8s-secret.yaml
# Apply deployment and service
echo "🔧 Deploying application..."
kubectl apply -f k8s-deployment.yaml
# Optional: Apply ingress
read -p "Do you want to deploy ingress? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "🌐 Deploying ingress..."
kubectl apply -f k8s-ingress.yaml
fi
# Check deployment status
echo "📊 Checking deployment status..."
kubectl rollout status deployment/proxy-detection-api
# Show service info
echo "📋 Service information:"
kubectl get service proxy-detection-service
echo "✅ Deployment completed!"
echo "💡 To test the API:"
echo " kubectl port-forward service/proxy-detection-service 8080:80"
echo " curl http://localhost:8080/health"

38
docker-compose.yml Normal file
View file

@ -0,0 +1,38 @@
version: '3.8'
services:
proxy-detection:
build: .
ports:
- "9999:9999"
environment:
- NODE_ENV=production
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9999/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
networks:
- proxy-net
# Optional: Add a reverse proxy for production
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
# - ./ssl:/etc/nginx/ssl:ro # Add SSL certificates here
depends_on:
- proxy-detection
networks:
- proxy-net
profiles:
- production
networks:
proxy-net:
driver: bridge

69
k8s-deployment.yaml Normal file
View file

@ -0,0 +1,69 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: proxy-detection-api
labels:
app: proxy-detection-api
spec:
replicas: 2
selector:
matchLabels:
app: proxy-detection-api
template:
metadata:
labels:
app: proxy-detection-api
spec:
containers:
- name: proxy-detection-api
image: your-registry/proxy-detection-api:latest
ports:
- containerPort: 2424
env:
- name: PORT
value: "2424"
- name: HOST
value: "0.0.0.0"
- name: NODE_ENV
value: "production"
- name: TRUST_PROXY
value: "true"
- name: API_KEY
valueFrom:
secretKeyRef:
name: proxy-detection-secret
key: api-key
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
livenessProbe:
httpGet:
path: /health
port: 2424
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 2424
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: proxy-detection-service
labels:
app: proxy-detection-api
spec:
selector:
app: proxy-detection-api
ports:
- port: 80
targetPort: 2424
protocol: TCP
type: ClusterIP

18
k8s-ingress.yaml Normal file
View file

@ -0,0 +1,18 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: proxy-detection-ingress
labels:
app: proxy-detection-api
spec:
rules:
- host: proxy-detection.stare.gg
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: proxy-detection-service
port:
number: 80

9
k8s-secret.yaml Normal file
View file

@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: proxy-detection-secret
type: Opaque
data:
# Base64 encoded API key (replace with your actual base64 encoded API key)
# To encode: echo -n "your-api-key" | base64
api-key: YmQ0MDZiZjUzZGRjNmFiZTFkOWRlNTkwNzgzMGE5NTU=

25
package.json Normal file
View file

@ -0,0 +1,25 @@
{
"name": "proxy-detection-api",
"version": "1.0.0",
"main": "src/index.ts",
"devDependencies": {
"@types/bun": "latest",
"@types/node": "^22.15.30",
"typescript": "^5.8.3"
},
"description": "Simple API to detect if IP is exposed when using a proxy",
"scripts": {
"dev": "bun run --watch src/index.ts",
"start": "bun run src/index.ts",
"build": "bun build src/index.ts --outdir ./dist --target bun",
"test": "echo \"No tests specified yet\" && exit 0",
"docker:build": "docker build -t proxy-detection-api .",
"docker:run": "docker run -p 9999:9999 proxy-detection-api",
"docker:dev": "docker-compose up --build",
"docker:prod": "docker-compose --profile production up -d"
},
"dependencies": {
"@fastify/cors": "^11.0.1",
"fastify": "^5.3.3"
}
}

134
src/index.ts Normal file
View file

@ -0,0 +1,134 @@
import Fastify, { FastifyRequest, FastifyReply } from 'fastify';
import { extractIPsFromHeaders } from './proxyService';
// Type definitions
interface ApiResponse {
success: boolean;
clientIP: string;
ipChain: string[];
foundIPs: string[];
totalFound: number;
headersSources: Record<string, string[]>;
allHeaders: Record<string, string>;
timestamp: number;
}
// Environment configuration
const API_KEY = process.env.API_KEY;
const PORT = parseInt(process.env.PORT || '2424');
const HOST = process.env.HOST || '0.0.0.0';
const NODE_ENV = process.env.NODE_ENV || 'production';
const objs: ApiResponse[] = []
// Create Fastify instance with trust proxy enabled
const fastify = Fastify({
logger: NODE_ENV === 'development',
trustProxy: process.env.TRUST_PROXY === 'true' || true
});
// API Key authentication middleware
fastify.addHook('preHandler', async (request: FastifyRequest, reply: FastifyReply) => {
// Skip auth for health check endpoint
if (request.url === '/health') {
return;
}
const apiKey = request.headers['x-api-key'] || (request.query as Record<string, any>)?.api_key;
if (!apiKey) {
reply.code(401).send({
error: 'API key required',
message: 'Please provide API key in X-API-Key header or api_key query parameter'
});
return;
}
if (apiKey !== API_KEY) {
reply.code(403).send({
error: 'Invalid API key',
message: 'The provided API key is not valid'
});
return;
}
});
// Helper function to get full IP chain
function getFullIPChain(request: FastifyRequest): { clientIP: string, ipChain: string[] } {
// Fastify automatically parses X-Forwarded-For when trustProxy is enabled
const clientIP = request.ip || 'unknown';
const ipChain = request.ips || []; // Full chain of IPs
return {
clientIP,
ipChain: ipChain.length > 0 ? ipChain : [clientIP]
};
}
// Start the server
const start = async () => {
try {
// Register CORS plugin
await fastify.register(import('@fastify/cors'), {
origin: true,
methods: ['GET', 'POST', 'OPTIONS']
});
// Health check endpoint (bypasses authentication)
fastify.get('/health', async () => {
return {
status: 'healthy',
timestamp: Date.now(),
uptime: process.uptime(),
version: '1.0.0'
};
});
// Random endpoint for testing
fastify.get('/random', async () => {
return objs[Math.round(objs.length * Math.random())] || { message: 'No data yet' };
});
// Main detection endpoint - extracts all IPs from headers
fastify.get('/', async (request) => {
const headers = request.headers;
const normalizedHeaders = Object.fromEntries(
Object.entries(headers).map(([key, value]) => [
key.toLowerCase(),
typeof value === 'string' ? value : String(value || '')
])
);
const result = extractIPsFromHeaders(normalizedHeaders);
const ipInfo = getFullIPChain(request);
const obj = {
success: true,
clientIP: ipInfo.clientIP, // Fastify's auto-detected client IP
ipChain: ipInfo.ipChain, // Full proxy chain
foundIPs: result.ips, // IPs found by our custom parser
totalFound: result.ips.length,
headersSources: result.sources,
allHeaders: normalizedHeaders,
timestamp: Date.now()
};
if( objs.length > 1000) {
objs.shift();
}
objs.push(obj);
return obj;
});
await fastify.listen({ port: PORT, host: HOST });
console.log(`🚀 Proxy Detection API running on http://localhost:${PORT}`);
console.log(`📍 Available endpoints:`);
console.log(` GET /health - Health check (no auth required)`);
console.log(` GET / - Extract all IP addresses from request headers`);
console.log(` GET /random - Get random cached result`);
console.log(`🔑 API Key required for protected endpoints`);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();

100
src/proxyService.ts Normal file
View file

@ -0,0 +1,100 @@
export interface HeaderIPResult {
ips: string[];
sources: Record<string, string[]>;
}
export function extractIPsFromHeaders(headers: Record<string, string | undefined>): HeaderIPResult {
const ips: string[] = [];
const sources: Record<string, string[]> = {};
// Common headers that might contain IP addresses
const ipHeaders = [
'x-forwarded-for',
'x-real-ip',
'x-client-ip',
'x-cluster-client-ip',
'x-forwarded',
'forwarded-for',
'forwarded',
'cf-connecting-ip',
'true-client-ip',
'x-originating-ip',
'x-remote-ip',
'x-remote-addr',
'client-ip',
'remote-addr',
'http-client-ip',
'http-x-forwarded-for',
'http-x-real-ip'
];
// IP regex pattern (basic IPv4 validation)
const ipRegex = /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/g;
// Check each header for IP addresses
for (const [headerName, headerValue] of Object.entries(headers)) {
if (!headerValue) continue;
// Check if this is a known IP header
if (ipHeaders.includes(headerName.toLowerCase())) {
const foundIPs = extractIPsFromValue(headerValue);
if (foundIPs.length > 0) {
sources[headerName] = foundIPs;
ips.push(...foundIPs);
}
}
// Also check for IP patterns in any header value
const matches = headerValue.match(ipRegex);
if (matches) {
const validIPs = matches.filter(isValidIP);
if (validIPs.length > 0) {
if (!sources[headerName]) {
sources[headerName] = [];
}
sources[headerName].push(...validIPs);
ips.push(...validIPs);
}
}
}
// Remove duplicates while preserving order
const uniqueIPs = [...new Set(ips)];
return {
ips: uniqueIPs,
sources
};
}
function extractIPsFromValue(value: string): string[] {
const ips: string[] = [];
// Handle comma-separated values (common in X-Forwarded-For)
const parts = value.split(',').map(part => part.trim());
for (const part of parts) {
// Handle "for=" syntax in Forwarded header
if (part.startsWith('for=')) {
const ip = part.substring(4).replace(/"/g, '');
if (isValidIP(ip)) {
ips.push(ip);
}
} else if (isValidIP(part)) {
ips.push(part);
}
}
return ips;
}
function isValidIP(ip: string): boolean {
// Basic IPv4 validation
const parts = ip.split('.');
if (parts.length !== 4) return false;
return parts.every(part => {
const num = parseInt(part, 10);
return !isNaN(num) && num >= 0 && num <= 255;
});
}