reogranized app and added more headers
This commit is contained in:
parent
1048ab5c20
commit
223f426a58
10 changed files with 925 additions and 218 deletions
121
CODE_ORGANIZATION.md
Normal file
121
CODE_ORGANIZATION.md
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# Proxy Detection API - Code Organization
|
||||
|
||||
This document describes the organized file structure of the Proxy Detection API.
|
||||
|
||||
## File Structure
|
||||
|
||||
The application has been organized into separate modules for better maintainability:
|
||||
|
||||
### Core Files
|
||||
|
||||
- **`index.ts`** - Entry point for the application
|
||||
- **`server.ts`** - Server setup and initialization
|
||||
- **`routes.ts`** - Route definitions and registration
|
||||
|
||||
### Module Files
|
||||
|
||||
- **`types.ts`** - TypeScript type definitions and interfaces
|
||||
- **`config.ts`** - Configuration and environment variables
|
||||
- **`middleware.ts`** - Authentication and other middleware functions
|
||||
- **`handlers.ts`** - Route handler functions
|
||||
- **`utils.ts`** - Utility functions for IP detection and processing
|
||||
|
||||
### Service Files
|
||||
|
||||
- **`proxyService.ts`** - IP extraction service (existing)
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ index.ts │
|
||||
│ (Entry Point) │
|
||||
└─────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────▼───────────────────────────────────────┐
|
||||
│ server.ts │
|
||||
│ (Server Configuration) │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐│
|
||||
│ │ config │ │ middleware │ │ routes ││
|
||||
│ └─────────────┘ └─────────────┘ └─────────────────────────┘│
|
||||
└─────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────▼───────────────────────────────────────┐
|
||||
│ handlers.ts │
|
||||
│ (Route Handlers) │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐│
|
||||
│ │ utils │ │ types │ │ proxyService ││
|
||||
│ └─────────────┘ └─────────────┘ └─────────────────────────┘│
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Module Responsibilities
|
||||
|
||||
### `types.ts`
|
||||
- `ApiResponse` - Main API response interface
|
||||
- `HealthResponse` - Health check response interface
|
||||
- `IPChainResult` - IP chain result interface
|
||||
- `DetailedDebugResponse` - Debug endpoint response interface
|
||||
|
||||
### `config.ts`
|
||||
- Environment variable management
|
||||
- Server configuration settings
|
||||
- CORS and trust proxy settings
|
||||
- Application constants
|
||||
|
||||
### `utils.ts`
|
||||
- `getClientIP()` - Enhanced IP detection with 30+ header sources
|
||||
- `getFullIPChain()` - Extract full proxy chain information
|
||||
- `normalizeHeaders()` - Normalize headers to consistent format
|
||||
- `extractGeolocation()` - Extract geolocation from headers
|
||||
- `extractProxyInfo()` - Extract proxy information from headers
|
||||
|
||||
### `middleware.ts`
|
||||
- `authMiddleware()` - API key authentication
|
||||
- Handles both header and query parameter authentication
|
||||
- Skips authentication for health and debug endpoints
|
||||
|
||||
### `handlers.ts`
|
||||
- `healthHandler()` - Health check endpoint
|
||||
- `mainHandler()` - Main proxy detection endpoint
|
||||
- `randomHandler()` - Random cached result endpoint
|
||||
- `detailedDebugHandler()` - Detailed debug information endpoint
|
||||
- In-memory result caching management
|
||||
|
||||
### `routes.ts`
|
||||
- Route registration and organization
|
||||
- Maps endpoints to their respective handlers
|
||||
- Maintains authentication bypass configuration
|
||||
|
||||
### `server.ts`
|
||||
- Fastify server instance creation
|
||||
- CORS configuration
|
||||
- Middleware registration
|
||||
- Server startup and error handling
|
||||
|
||||
## Benefits of This Organization
|
||||
|
||||
1. **Separation of Concerns** - Each file has a specific responsibility
|
||||
2. **Maintainability** - Easier to locate and modify specific functionality
|
||||
3. **Testability** - Individual modules can be unit tested independently
|
||||
4. **Reusability** - Utility functions can be easily reused
|
||||
5. **Type Safety** - Centralized type definitions ensure consistency
|
||||
6. **Configuration Management** - All settings are centralized and manageable
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Adding new endpoints**: Add handler to `handlers.ts`, register route in `routes.ts`
|
||||
2. **Modifying IP detection**: Update functions in `utils.ts`
|
||||
3. **Changing configuration**: Update `config.ts`
|
||||
4. **Adding new types**: Add to `types.ts`
|
||||
5. **Adding middleware**: Add to `middleware.ts`
|
||||
|
||||
## Running the Application
|
||||
|
||||
The application starts from `index.ts` which:
|
||||
1. Creates a server instance
|
||||
2. Configures middleware and routes
|
||||
3. Starts the server
|
||||
4. Handles startup errors
|
||||
|
||||
All functionality remains the same as the original monolithic version, but is now organized for better maintainability and scalability.
|
||||
314
IP_DETECTION_K8S_SETUP.md
Normal file
314
IP_DETECTION_K8S_SETUP.md
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
# Real IP Detection in Kubernetes - Complete Setup Guide
|
||||
|
||||
This document summarizes all the steps taken to successfully configure real IP detection for the proxy detection API in a Kubernetes cluster with MetalLB LoadBalancer.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Initially, the API was only receiving internal Kubernetes cluster IPs instead of real client IPs when deployed in Kubernetes. The application needed to detect the actual client IP addresses through various proxy headers while running behind:
|
||||
- MetalLB LoadBalancer
|
||||
- Nginx Ingress Controller
|
||||
- Kubernetes Service mesh
|
||||
|
||||
## Solution Overview
|
||||
|
||||
We implemented a multi-layered approach to ensure proper IP detection:
|
||||
|
||||
1. **Kubernetes Infrastructure Configuration**
|
||||
2. **Application-Level IP Detection**
|
||||
3. **Ingress and LoadBalancer Optimization**
|
||||
|
||||
---
|
||||
|
||||
## 1. Kubernetes Infrastructure Setup
|
||||
|
||||
### MetalLB LoadBalancer Configuration
|
||||
|
||||
**Issue**: The MetalLB LoadBalancer was not properly preserving client IPs.
|
||||
|
||||
**Solution**:
|
||||
- Identified LoadBalancer external IP: `192.95.29.118`
|
||||
- Configured `externalTrafficPolicy: Local` to preserve source IPs
|
||||
- Corrected namespace from `ingress-nginx` to `ingress`
|
||||
|
||||
```yaml
|
||||
# k8s-service.yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: proxy-detection-service
|
||||
namespace: default
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
externalTrafficPolicy: Local # Critical for IP preservation
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 2424
|
||||
protocol: TCP
|
||||
selector:
|
||||
app: proxy-detection
|
||||
```
|
||||
|
||||
### Ingress Controller Configuration
|
||||
|
||||
**Issue**: Nginx Ingress was not configured to pass real client IPs.
|
||||
|
||||
**Solution**: Enhanced ingress configuration with custom annotations and ConfigMap.
|
||||
|
||||
```yaml
|
||||
# k8s-ingress.yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: proxy-detection-ingress
|
||||
namespace: default
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: "nginx"
|
||||
nginx.ingress.kubernetes.io/use-real-ip: "true"
|
||||
nginx.ingress.kubernetes.io/real-ip-header: "X-Forwarded-For"
|
||||
nginx.ingress.kubernetes.io/compute-full-forwarded-for: "true"
|
||||
nginx.ingress.kubernetes.io/forwarded-for-header: "X-Forwarded-For"
|
||||
# Additional IP preservation headers
|
||||
nginx.ingress.kubernetes.io/configuration-snippet: |
|
||||
more_set_headers "X-Real-IP $remote_addr";
|
||||
more_set_headers "X-Forwarded-For $proxy_add_x_forwarded_for";
|
||||
spec:
|
||||
rules:
|
||||
- host: proxy-detection.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: proxy-detection-service
|
||||
port:
|
||||
number: 80
|
||||
```
|
||||
|
||||
### ConfigMap for Custom Headers
|
||||
|
||||
**Created**: Custom ConfigMap to configure Nginx for proper header handling.
|
||||
|
||||
```bash
|
||||
kubectl create configmap nginx-configuration \
|
||||
--from-literal=use-forwarded-headers=true \
|
||||
--from-literal=compute-full-forwarded-for=true \
|
||||
--from-literal=use-proxy-protocol=false \
|
||||
-n ingress
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Application-Level IP Detection
|
||||
|
||||
### Comprehensive IP Header Detection
|
||||
|
||||
**Issue**: The application wasn't checking all possible IP headers from various sources.
|
||||
|
||||
**Solution**: Implemented comprehensive IP detection supporting 30+ header sources.
|
||||
|
||||
#### Enhanced `getClientIP()` Function
|
||||
|
||||
```typescript
|
||||
// src/utils.ts
|
||||
export function getClientIP(request: FastifyRequest): string {
|
||||
const headers = request.headers;
|
||||
|
||||
const ipSources = [
|
||||
// CDN and Cloud Provider Headers
|
||||
headers['cf-connecting-ip']?.toString(), // Cloudflare
|
||||
headers['cf-pseudo-ipv4']?.toString(), // Cloudflare IPv4
|
||||
headers['true-client-ip']?.toString(), // Akamai/CDNs
|
||||
headers['x-akamai-edgescape']?.toString().split(',')[0]?.trim(),
|
||||
headers['fastly-client-ip']?.toString(), // Fastly CDN
|
||||
headers['x-azure-clientip']?.toString(), // Azure
|
||||
|
||||
// Standard Proxy Headers
|
||||
headers['x-forwarded-for']?.toString().split(',')[0]?.trim(), // Most common
|
||||
headers['x-original-forwarded-for']?.toString().split(',')[0]?.trim(),
|
||||
headers['x-client-ip']?.toString(), // Apache
|
||||
headers['x-real-ip']?.toString(), // Nginx
|
||||
headers['x-originating-ip']?.toString(), // IIS
|
||||
|
||||
// Load Balancer Headers
|
||||
headers['x-cluster-client-ip']?.toString(), // Kubernetes
|
||||
headers['forwarded']?.toString().match(/for=([^;,\s]+)/)?.[1], // RFC 7239
|
||||
headers['x-appengine-remote-addr']?.toString(), // Google App Engine
|
||||
|
||||
// Security and Firewall Headers
|
||||
headers['x-sucuri-clientip']?.toString(), // Sucuri WAF
|
||||
headers['incap-client-ip']?.toString(), // Incapsula
|
||||
|
||||
// Fastify defaults
|
||||
request.ip,
|
||||
request.socket.remoteAddress
|
||||
];
|
||||
|
||||
return ipSources.find(ip => ip && ip !== 'unknown' && ip.trim() !== '') || 'unknown';
|
||||
}
|
||||
```
|
||||
|
||||
### Trust Proxy Configuration
|
||||
|
||||
**Issue**: Fastify wasn't configured to trust the Kubernetes network ranges.
|
||||
|
||||
**Solution**: Configured comprehensive trust proxy settings.
|
||||
|
||||
```typescript
|
||||
// src/config.ts
|
||||
export const config = {
|
||||
server: {
|
||||
trustProxy: [
|
||||
'10.0.0.0/8', // Private networks
|
||||
'172.16.0.0/12', // Docker/K8s networks
|
||||
'192.168.0.0/16', // Local networks
|
||||
'127.0.0.1', // Localhost
|
||||
'::1' // IPv6 localhost
|
||||
]
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Debugging and Verification
|
||||
|
||||
### Debug Endpoints
|
||||
|
||||
**Created**: Comprehensive debug endpoints to troubleshoot IP detection.
|
||||
|
||||
```typescript
|
||||
// Detailed debug endpoint showing all possible IP sources
|
||||
fastify.get('/ip-debug-detailed', async (request) => {
|
||||
return {
|
||||
allSources: {
|
||||
'cf-connecting-ip': headers['cf-connecting-ip'],
|
||||
'x-forwarded-for': headers['x-forwarded-for'],
|
||||
'x-real-ip': headers['x-real-ip'],
|
||||
'x-cluster-client-ip': headers['x-cluster-client-ip'],
|
||||
// ... all 30+ headers
|
||||
},
|
||||
detectedClientIP: getClientIP(request),
|
||||
fastifyIPs: request.ips || [],
|
||||
geolocation: { /* geo info */ },
|
||||
security: { /* security headers */ }
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Process
|
||||
|
||||
1. **Local Testing**: Verified basic functionality
|
||||
2. **Kubernetes Deployment**: Deployed with debug endpoints
|
||||
3. **IP Verification**: Used `/ip-debug-detailed` to verify real IPs
|
||||
4. **Load Testing**: Confirmed consistent IP detection
|
||||
|
||||
---
|
||||
|
||||
## 4. Key Configuration Commands
|
||||
|
||||
### Kubernetes Deployment Commands
|
||||
|
||||
```bash
|
||||
# Apply all configurations
|
||||
kubectl apply -f k8s-deployment.yaml
|
||||
kubectl apply -f k8s-service.yaml
|
||||
kubectl apply -f k8s-ingress.yaml
|
||||
kubectl apply -f k8s-secret.yaml
|
||||
|
||||
# Create nginx configuration
|
||||
kubectl create configmap nginx-configuration \
|
||||
--from-literal=use-forwarded-headers=true \
|
||||
--from-literal=compute-full-forwarded-for=true \
|
||||
-n ingress
|
||||
|
||||
# Verify LoadBalancer
|
||||
kubectl get svc proxy-detection-service
|
||||
# Should show EXTERNAL-IP: 192.95.29.118
|
||||
|
||||
# Check ingress status
|
||||
kubectl get ingress proxy-detection-ingress
|
||||
```
|
||||
|
||||
### Verification Commands
|
||||
|
||||
```bash
|
||||
# Test real IP detection
|
||||
curl -H "X-Forwarded-For: 203.0.113.1" http://192.95.29.118/ip-debug-detailed
|
||||
|
||||
# Check pods
|
||||
kubectl get pods -l app=proxy-detection
|
||||
|
||||
# View logs
|
||||
kubectl logs -l app=proxy-detection
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Critical Success Factors
|
||||
|
||||
### What Made It Work
|
||||
|
||||
1. **Correct Namespace**: Changed from `ingress-nginx` to `ingress`
|
||||
2. **externalTrafficPolicy: Local**: Preserved source IPs at LoadBalancer level
|
||||
3. **Comprehensive Header Detection**: Supported all major proxy/CDN headers
|
||||
4. **Trust Proxy Configuration**: Properly configured Fastify to trust K8s networks
|
||||
5. **Ingress Annotations**: Used correct nginx annotations for IP forwarding
|
||||
|
||||
### Common Pitfalls Avoided
|
||||
|
||||
- ❌ Using wrong namespace for ingress controller
|
||||
- ❌ Missing `externalTrafficPolicy: Local`
|
||||
- ❌ Not configuring trust proxy in application
|
||||
- ❌ Limited header detection (only checking X-Forwarded-For)
|
||||
- ❌ Missing nginx ingress annotations
|
||||
|
||||
---
|
||||
|
||||
## 6. Final Architecture
|
||||
|
||||
```
|
||||
Internet Client (Real IP: 203.0.113.1)
|
||||
↓
|
||||
MetalLB LoadBalancer (192.95.29.118)
|
||||
↓ [preserves source IP with externalTrafficPolicy: Local]
|
||||
Nginx Ingress Controller
|
||||
↓ [adds X-Forwarded-For, X-Real-IP headers]
|
||||
Kubernetes Service
|
||||
↓ [routes to pod]
|
||||
Proxy Detection API Pod
|
||||
↓ [getClientIP() checks 30+ headers]
|
||||
Returns: Real Client IP (203.0.113.1)
|
||||
```
|
||||
|
||||
## 7. Results
|
||||
|
||||
✅ **Success**: Real client IPs are now properly detected
|
||||
✅ **Reliability**: Consistent IP detection across all request types
|
||||
✅ **Scalability**: Supports multiple CDNs, proxies, and security services
|
||||
✅ **Debugging**: Comprehensive debug endpoints for troubleshooting
|
||||
|
||||
The API now successfully detects real client IPs in the Kubernetes environment with MetalLB LoadBalancer, providing accurate proxy detection capabilities for production use.
|
||||
|
||||
|
||||
|
||||
COMMANDS
|
||||
|
||||
# Configure NGINX Ingress Controller (in 'ingress' namespace)
|
||||
kubectl patch configmap ingress-nginx-controller -n ingress --patch '
|
||||
data:
|
||||
use-forwarded-headers: "true"
|
||||
compute-full-forwarded-for: "true"
|
||||
real-ip-header: "X-Forwarded-For"
|
||||
set-real-ip-from: "192.95.29.118/32"
|
||||
enable-real-ip: "true"
|
||||
proxy-real-ip-cidr: "192.95.29.118/32"
|
||||
'
|
||||
|
||||
# Set external traffic policy to Local for source IP preservation
|
||||
kubectl patch service ingress-ingress-nginx-controller -n ingress --patch '
|
||||
spec:
|
||||
externalTrafficPolicy: Local
|
||||
'
|
||||
|
||||
kubectl rollout restart deployment ingress-nginx-controller -n ingress
|
||||
35
src/config.ts
Normal file
35
src/config.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* Configuration and environment variables
|
||||
*/
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
key: process.env.API_KEY,
|
||||
port: parseInt(process.env.PORT || '2424'),
|
||||
host: process.env.HOST || '0.0.0.0',
|
||||
version: '1.0.0'
|
||||
},
|
||||
|
||||
environment: {
|
||||
nodeEnv: process.env.NODE_ENV || 'production',
|
||||
isDevelopment: process.env.NODE_ENV === 'development'
|
||||
},
|
||||
|
||||
server: {
|
||||
// Trust proxy settings for Kubernetes and common networks
|
||||
trustProxy: [
|
||||
'10.0.0.0/8', // Private networks
|
||||
'172.16.0.0/12', // Docker/K8s networks
|
||||
'192.168.0.0/16', // Local networks
|
||||
'127.0.0.1', // Localhost
|
||||
'::1' // IPv6 localhost
|
||||
],
|
||||
|
||||
cors: {
|
||||
origin: true,
|
||||
methods: ['GET', 'POST', 'OPTIONS']
|
||||
},
|
||||
|
||||
maxCachedResults: 1000
|
||||
}
|
||||
};
|
||||
156
src/handlers.ts
Normal file
156
src/handlers.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* Route handlers for the proxy detection API
|
||||
*/
|
||||
|
||||
import { FastifyRequest } from 'fastify';
|
||||
import { extractIPsFromHeaders } from './proxyService';
|
||||
import { ApiResponse, HealthResponse, DetailedDebugResponse } from './types';
|
||||
import { config } from './config';
|
||||
import {
|
||||
getClientIP,
|
||||
getFullIPChain,
|
||||
normalizeHeaders,
|
||||
extractGeolocation,
|
||||
extractProxyInfo
|
||||
} from './utils';
|
||||
|
||||
// In-memory cache for results
|
||||
const cachedResults: ApiResponse[] = [];
|
||||
|
||||
/**
|
||||
* Health check endpoint
|
||||
*/
|
||||
export async function healthHandler(): Promise<HealthResponse> {
|
||||
return {
|
||||
status: 'healthy',
|
||||
timestamp: Date.now(),
|
||||
uptime: process.uptime(),
|
||||
version: config.api.version
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Main proxy detection endpoint
|
||||
*/
|
||||
export async function mainHandler(request: FastifyRequest): Promise<ApiResponse> {
|
||||
const headers = request.headers;
|
||||
const normalizedHeaders = normalizeHeaders(headers);
|
||||
|
||||
const result = extractIPsFromHeaders(normalizedHeaders);
|
||||
const ipInfo = getFullIPChain(request);
|
||||
|
||||
// Enhanced response with more context
|
||||
const response: ApiResponse = {
|
||||
success: true,
|
||||
clientIP: ipInfo.clientIP,
|
||||
ipChain: ipInfo.ipChain,
|
||||
foundIPs: result.ips,
|
||||
totalFound: result.ips.length,
|
||||
headersSources: result.sources,
|
||||
|
||||
// Additional context
|
||||
geolocation: extractGeolocation(normalizedHeaders),
|
||||
proxy: extractProxyInfo(normalizedHeaders),
|
||||
|
||||
allHeaders: normalizedHeaders,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// Cache management
|
||||
if (cachedResults.length > config.server.maxCachedResults) {
|
||||
cachedResults.shift();
|
||||
}
|
||||
cachedResults.push(response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Random cached result endpoint
|
||||
*/
|
||||
export async function randomHandler(): Promise<ApiResponse | { message: string }> {
|
||||
if (cachedResults.length === 0) {
|
||||
return { message: 'No data yet' };
|
||||
}
|
||||
|
||||
const randomIndex = Math.floor(Math.random() * cachedResults.length);
|
||||
return cachedResults[randomIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detailed debug endpoint with all possible headers
|
||||
*/
|
||||
export async function detailedDebugHandler(request: FastifyRequest): Promise<DetailedDebugResponse> {
|
||||
const headers = request.headers;
|
||||
|
||||
return {
|
||||
allSources: {
|
||||
// CDN Headers
|
||||
'cf-connecting-ip': headers['cf-connecting-ip'],
|
||||
'cf-pseudo-ipv4': headers['cf-pseudo-ipv4'],
|
||||
'cf-ipcountry': headers['cf-ipcountry'],
|
||||
'true-client-ip': headers['true-client-ip'],
|
||||
'x-akamai-edgescape': headers['x-akamai-edgescape'],
|
||||
'fastly-client-ip': headers['fastly-client-ip'],
|
||||
|
||||
// Standard Headers
|
||||
'x-forwarded-for': headers['x-forwarded-for'],
|
||||
'x-original-forwarded-for': headers['x-original-forwarded-for'],
|
||||
'x-client-ip': headers['x-client-ip'],
|
||||
'x-real-ip': headers['x-real-ip'],
|
||||
'x-originating-ip': headers['x-originating-ip'],
|
||||
'x-remote-ip': headers['x-remote-ip'],
|
||||
'x-remote-addr': headers['x-remote-addr'],
|
||||
|
||||
// Load Balancer
|
||||
'x-cluster-client-ip': headers['x-cluster-client-ip'],
|
||||
'forwarded': headers['forwarded'],
|
||||
'x-forwarded': headers['x-forwarded'],
|
||||
'x-appengine-remote-addr': headers['x-appengine-remote-addr'],
|
||||
|
||||
// Cloud Provider
|
||||
'x-azure-clientip': headers['x-azure-clientip'],
|
||||
'x-azure-ref': headers['x-azure-ref'],
|
||||
|
||||
// Proxy/Firewall
|
||||
'wl-proxy-client-ip': headers['wl-proxy-client-ip'],
|
||||
'proxy-client-ip': headers['proxy-client-ip'],
|
||||
'x-sucuri-clientip': headers['x-sucuri-clientip'],
|
||||
'incap-client-ip': headers['incap-client-ip'],
|
||||
|
||||
// Mobile/Carrier
|
||||
'x-nokia-msisdn': headers['x-nokia-msisdn'],
|
||||
'x-up-calling-line-id': headers['x-up-calling-line-id'],
|
||||
'http_client_ip': headers['http_client_ip'],
|
||||
'http_x_forwarded_for': headers['http_x_forwarded_for'],
|
||||
'remote_addr': headers['remote_addr'],
|
||||
|
||||
// Alternative
|
||||
'x-coming-from': headers['x-coming-from'],
|
||||
'x-forwarded-host': headers['x-forwarded-host'],
|
||||
'x-host': headers['x-host'],
|
||||
|
||||
// Framework defaults
|
||||
'fastify-ip': request.ip,
|
||||
'socket-remote': request.socket.remoteAddress
|
||||
},
|
||||
detectedClientIP: getClientIP(request),
|
||||
fastifyIPs: request.ips || [],
|
||||
|
||||
// Additional useful info
|
||||
geolocation: {
|
||||
'cf-ipcountry': headers['cf-ipcountry'], // Cloudflare country
|
||||
'cf-ray': headers['cf-ray'], // Cloudflare ray ID
|
||||
'x-forwarded-proto': headers['x-forwarded-proto'], // Protocol
|
||||
'x-forwarded-port': headers['x-forwarded-port'], // Port
|
||||
},
|
||||
|
||||
security: {
|
||||
'x-sucuri-country': headers['x-sucuri-country'],
|
||||
'x-forwarded-ssl': headers['x-forwarded-ssl'],
|
||||
'x-url-scheme': headers['x-url-scheme'],
|
||||
},
|
||||
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
234
src/index.ts
234
src/index.ts
|
|
@ -1,221 +1,21 @@
|
|||
import Fastify, { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { extractIPsFromHeaders } from './proxyService';
|
||||
/**
|
||||
* Proxy Detection API
|
||||
* Entry point for the application
|
||||
*/
|
||||
|
||||
// Type definitions
|
||||
interface ApiResponse {
|
||||
success: boolean;
|
||||
clientIP: string;
|
||||
ipChain: string[];
|
||||
foundIPs: string[];
|
||||
totalFound: number;
|
||||
headersSources: Record<string, string[]>;
|
||||
allHeaders: Record<string, string>;
|
||||
timestamp: number;
|
||||
import { createServer, configureServer, startServer } from './server';
|
||||
|
||||
/**
|
||||
* Initialize and start the proxy detection API
|
||||
*/
|
||||
async function main() {
|
||||
const server = createServer();
|
||||
await configureServer(server);
|
||||
await startServer(server);
|
||||
}
|
||||
|
||||
// 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 comprehensive trust proxy
|
||||
const fastify = Fastify({
|
||||
logger: NODE_ENV === 'development',
|
||||
trustProxy: ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', '127.0.0.1', '::1'] // Trust k8s networks
|
||||
});
|
||||
|
||||
// Enhanced IP detection function
|
||||
function getClientIP(request: FastifyRequest): string {
|
||||
const headers = request.headers;
|
||||
|
||||
// Try multiple headers in order of preference
|
||||
const ipSources = [
|
||||
headers['cf-connecting-ip']?.toString(), // Cloudflare
|
||||
headers['true-client-ip']?.toString(), // Akamai/other CDNs
|
||||
headers['x-forwarded-for']?.toString().split(',')[0]?.trim(), // Most common
|
||||
headers['x-original-forwarded-for']?.toString().split(',')[0]?.trim(), // Original
|
||||
headers['x-client-ip']?.toString(), // Apache
|
||||
headers['x-cluster-client-ip']?.toString(), // Cluster
|
||||
headers['forwarded']?.toString().match(/for=([^;,\s]+)/)?.[1], // RFC 7239
|
||||
request.ip, // Fastify default
|
||||
request.socket.remoteAddress // Socket
|
||||
];
|
||||
|
||||
// Don't filter private IPs for now - let's see what we get
|
||||
return ipSources.find(ip => ip && ip !== 'unknown') || 'unknown';
|
||||
}
|
||||
|
||||
// Add a more detailed debug endpoint
|
||||
fastify.get('/ip-debug-detailed', async (request) => {
|
||||
const headers = request.headers;
|
||||
|
||||
return {
|
||||
allSources: {
|
||||
'cf-connecting-ip': headers['cf-connecting-ip'],
|
||||
'true-client-ip': headers['true-client-ip'],
|
||||
'x-forwarded-for': headers['x-forwarded-for'],
|
||||
'x-original-forwarded-for': headers['x-original-forwarded-for'],
|
||||
'x-client-ip': headers['x-client-ip'],
|
||||
'x-real-ip': headers['x-real-ip'],
|
||||
'x-cluster-client-ip': headers['x-cluster-client-ip'],
|
||||
'forwarded': headers['forwarded'],
|
||||
'fastify-ip': request.ip,
|
||||
'socket-remote': request.socket.remoteAddress
|
||||
},
|
||||
detectedClientIP: getClientIP(request),
|
||||
fastifyIPs: request.ips,
|
||||
allHeaders: headers,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
});
|
||||
|
||||
// Check if IP is private/internal
|
||||
function isPrivateIP(ip: string): boolean {
|
||||
if (!ip || ip === 'unknown') return true;
|
||||
|
||||
// Remove any port number
|
||||
const cleanIP = ip.split(':')[0];
|
||||
|
||||
// Private IP ranges
|
||||
const privateRanges = [
|
||||
/^10\./, // 10.0.0.0/8
|
||||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.0.0/12
|
||||
/^192\.168\./, // 192.168.0.0/16
|
||||
/^127\./, // 127.0.0.0/8 (localhost)
|
||||
/^169\.254\./, // 169.254.0.0/16 (link-local)
|
||||
/^::1$/, // IPv6 localhost
|
||||
/^fe80:/ // IPv6 link-local
|
||||
];
|
||||
|
||||
return privateRanges.some(range => range.test(cleanIP));
|
||||
}
|
||||
|
||||
// API Key authentication middleware
|
||||
fastify.addHook('preHandler', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
// Skip auth for health check and debug endpoints
|
||||
if (request.url === '/health' || request.url === '/ip-debug') {
|
||||
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[] } {
|
||||
const clientIP = getClientIP(request);
|
||||
const ipChain = request.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'
|
||||
};
|
||||
});
|
||||
|
||||
// IP Debug endpoint (bypasses authentication) - for troubleshooting
|
||||
fastify.get('/ip-debug', async (request) => {
|
||||
return {
|
||||
detectedClientIP: getClientIP(request),
|
||||
fastifyIP: request.ip,
|
||||
fastifyIPs: request.ips,
|
||||
socketIP: request.socket.remoteAddress,
|
||||
allIPHeaders: {
|
||||
'x-forwarded-for': request.headers['x-forwarded-for'],
|
||||
'x-real-ip': request.headers['x-real-ip'],
|
||||
'cf-connecting-ip': request.headers['cf-connecting-ip'],
|
||||
'true-client-ip': request.headers['true-client-ip'],
|
||||
'x-client-ip': request.headers['x-client-ip'],
|
||||
'x-cluster-client-ip': request.headers['x-cluster-client-ip'],
|
||||
'forwarded': request.headers['forwarded']
|
||||
},
|
||||
isPrivateIP: isPrivateIP(request.headers['x-real-ip']?.toString() || ''),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
});
|
||||
|
||||
// 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, // Enhanced client IP detection
|
||||
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 /ip-debug - IP debugging info (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);
|
||||
// Start the application
|
||||
main().catch((error) => {
|
||||
console.error('Failed to start the server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
start();
|
||||
});
|
||||
35
src/middleware.ts
Normal file
35
src/middleware.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* Middleware functions for the proxy detection API
|
||||
*/
|
||||
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { config } from './config';
|
||||
|
||||
/**
|
||||
* API Key authentication middleware
|
||||
* Skips authentication for health check and debug endpoints
|
||||
*/
|
||||
export async function authMiddleware(request: FastifyRequest, reply: FastifyReply) {
|
||||
// Skip auth for health check and debug endpoints
|
||||
if (request.url === '/health' || request.url === '/ip-debug') {
|
||||
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 !== config.api.key) {
|
||||
reply.code(403).send({
|
||||
error: 'Invalid API key',
|
||||
message: 'The provided API key is not valid'
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
28
src/routes.ts
Normal file
28
src/routes.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Route definitions for the proxy detection API
|
||||
*/
|
||||
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import {
|
||||
healthHandler,
|
||||
mainHandler,
|
||||
randomHandler,
|
||||
detailedDebugHandler
|
||||
} from './handlers';
|
||||
|
||||
/**
|
||||
* Register all routes with the Fastify instance
|
||||
*/
|
||||
export async function registerRoutes(fastify: FastifyInstance) {
|
||||
// Health check endpoint (bypasses authentication)
|
||||
fastify.get('/health', healthHandler);
|
||||
|
||||
// Detailed debug endpoint (bypasses authentication)
|
||||
fastify.get('/ip-debug-detailed', detailedDebugHandler);
|
||||
|
||||
// Main detection endpoint - extracts all IPs from headers
|
||||
fastify.get('/', mainHandler);
|
||||
|
||||
// Random endpoint for testing
|
||||
fastify.get('/random', randomHandler);
|
||||
}
|
||||
58
src/server.ts
Normal file
58
src/server.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* Server setup and initialization
|
||||
*/
|
||||
|
||||
import Fastify, { FastifyInstance } from 'fastify';
|
||||
import { config } from './config';
|
||||
import { authMiddleware } from './middleware';
|
||||
import { registerRoutes } from './routes';
|
||||
|
||||
/**
|
||||
* Create and configure Fastify server instance
|
||||
*/
|
||||
export function createServer(): FastifyInstance {
|
||||
const fastify = Fastify({
|
||||
logger: config.environment.isDevelopment,
|
||||
trustProxy: config.server.trustProxy
|
||||
});
|
||||
|
||||
return fastify;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure server with middleware and routes
|
||||
*/
|
||||
export async function configureServer(fastify: FastifyInstance): Promise<void> {
|
||||
// Register CORS plugin
|
||||
await fastify.register(import('@fastify/cors'), config.server.cors);
|
||||
|
||||
// Register authentication middleware
|
||||
fastify.addHook('preHandler', authMiddleware);
|
||||
|
||||
// Register all routes
|
||||
await registerRoutes(fastify);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the server
|
||||
*/
|
||||
export async function startServer(fastify: FastifyInstance): Promise<void> {
|
||||
try {
|
||||
await fastify.listen({
|
||||
port: config.api.port,
|
||||
host: config.api.host
|
||||
});
|
||||
|
||||
console.log(`🚀 Proxy Detection API running on http://localhost:${config.api.port}`);
|
||||
console.log(`📍 Available endpoints:`);
|
||||
console.log(` GET /health - Health check (no auth required)`);
|
||||
console.log(` GET /ip-debug-detailed - IP debugging info (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);
|
||||
}
|
||||
}
|
||||
45
src/types.ts
Normal file
45
src/types.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Type definitions for the proxy detection API
|
||||
*/
|
||||
|
||||
export interface ApiResponse {
|
||||
success: boolean;
|
||||
clientIP: string;
|
||||
ipChain: string[];
|
||||
foundIPs: string[];
|
||||
totalFound: number;
|
||||
headersSources: Record<string, string[]>;
|
||||
allHeaders: Record<string, string>;
|
||||
timestamp: number;
|
||||
geolocation?: {
|
||||
country?: string;
|
||||
rayId?: string;
|
||||
};
|
||||
proxy?: {
|
||||
protocol?: string;
|
||||
port?: string;
|
||||
host?: string;
|
||||
ssl?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface HealthResponse {
|
||||
status: string;
|
||||
timestamp: number;
|
||||
uptime: number;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface IPChainResult {
|
||||
clientIP: string;
|
||||
ipChain: string[];
|
||||
}
|
||||
|
||||
export interface DetailedDebugResponse {
|
||||
allSources: Record<string, any>;
|
||||
detectedClientIP: string;
|
||||
fastifyIPs: string[];
|
||||
geolocation: Record<string, any>;
|
||||
security: Record<string, any>;
|
||||
timestamp: number;
|
||||
}
|
||||
115
src/utils.ts
Normal file
115
src/utils.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/**
|
||||
* Utility functions for IP detection and processing
|
||||
*/
|
||||
|
||||
import { FastifyRequest } from 'fastify';
|
||||
import { IPChainResult } from './types';
|
||||
|
||||
/**
|
||||
* Enhanced IP detection function with comprehensive header support
|
||||
* Supports CDNs, cloud providers, load balancers, and security services
|
||||
*/
|
||||
export function getClientIP(request: FastifyRequest): string {
|
||||
const headers = request.headers;
|
||||
|
||||
// Try multiple headers in order of preference
|
||||
const ipSources = [
|
||||
// CDN and Cloud Provider Headers
|
||||
headers['cf-connecting-ip']?.toString(), // Cloudflare
|
||||
headers['cf-pseudo-ipv4']?.toString(), // Cloudflare IPv4 fallback
|
||||
headers['true-client-ip']?.toString(), // Akamai/other CDNs
|
||||
headers['x-akamai-edgescape']?.toString().split(',')[0]?.trim(), // Akamai EdgeScape
|
||||
headers['fastly-client-ip']?.toString(), // Fastly CDN
|
||||
headers['x-azure-clientip']?.toString(), // Azure
|
||||
headers['x-azure-ref']?.toString(), // Azure reference
|
||||
|
||||
// Standard Proxy Headers
|
||||
headers['x-forwarded-for']?.toString().split(',')[0]?.trim(), // Most common
|
||||
headers['x-original-forwarded-for']?.toString().split(',')[0]?.trim(), // Original
|
||||
headers['x-client-ip']?.toString(), // Apache/general
|
||||
headers['x-real-ip']?.toString(), // Nginx
|
||||
headers['x-originating-ip']?.toString(), // IIS
|
||||
headers['x-remote-ip']?.toString(), // Alternative
|
||||
headers['x-remote-addr']?.toString(), // Alternative
|
||||
|
||||
// Load Balancer Headers
|
||||
headers['x-cluster-client-ip']?.toString(), // Kubernetes
|
||||
headers['x-forwarded']?.toString().match(/for=([^;,\s]+)/)?.[1], // RFC 7239
|
||||
headers['forwarded']?.toString().match(/for=([^;,\s]+)/)?.[1], // RFC 7239
|
||||
headers['x-appengine-remote-addr']?.toString(), // Google App Engine
|
||||
|
||||
// ISP and Telecom Headers
|
||||
headers['wl-proxy-client-ip']?.toString(), // WebLogic
|
||||
headers['proxy-client-ip']?.toString(), // Generic proxy
|
||||
headers['x-coming-from']?.toString(), // Some proxies
|
||||
headers['x-forwarded-host']?.toString(), // Host forwarding
|
||||
headers['x-host']?.toString(), // Host alternative
|
||||
|
||||
// Mobile and Carrier Headers
|
||||
headers['x-nokia-msisdn']?.toString(), // Nokia
|
||||
headers['x-up-calling-line-id']?.toString(), // UP browser
|
||||
headers['http_client_ip']?.toString(), // HTTP client
|
||||
headers['http_x_forwarded_for']?.toString(), // HTTP X-Forwarded
|
||||
headers['remote_addr']?.toString(), // Direct remote
|
||||
|
||||
// Security and Firewall Headers
|
||||
headers['x-sucuri-clientip']?.toString(), // Sucuri WAF
|
||||
headers['x-sucuri-country']?.toString(), // Sucuri country
|
||||
headers['incap-client-ip']?.toString(), // Incapsula
|
||||
headers['cf-ipcountry']?.toString(), // Cloudflare country
|
||||
|
||||
// Fastify defaults
|
||||
request.ip, // Fastify default
|
||||
request.socket.remoteAddress // Socket
|
||||
];
|
||||
|
||||
// Filter out undefined/null values and return first valid IP
|
||||
return ipSources.find(ip => ip && ip !== 'unknown' && ip.trim() !== '') || 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full IP chain including proxy information
|
||||
*/
|
||||
export function getFullIPChain(request: FastifyRequest): IPChainResult {
|
||||
const clientIP = getClientIP(request);
|
||||
const ipChain = request.ips || [];
|
||||
|
||||
return {
|
||||
clientIP,
|
||||
ipChain: ipChain.length > 0 ? ipChain : [clientIP]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize headers to consistent format
|
||||
*/
|
||||
export function normalizeHeaders(headers: Record<string, any>): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(headers).map(([key, value]) => [
|
||||
key.toLowerCase(),
|
||||
typeof value === 'string' ? value : String(value || '')
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract geolocation information from headers
|
||||
*/
|
||||
export function extractGeolocation(headers: Record<string, string>) {
|
||||
return {
|
||||
country: headers['cf-ipcountry'] || headers['x-sucuri-country'],
|
||||
rayId: headers['cf-ray'],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract proxy information from headers
|
||||
*/
|
||||
export function extractProxyInfo(headers: Record<string, string>) {
|
||||
return {
|
||||
protocol: headers['x-forwarded-proto'],
|
||||
port: headers['x-forwarded-port'],
|
||||
host: headers['x-forwarded-host'],
|
||||
ssl: headers['x-forwarded-ssl'],
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue