From 0c795faf476c4ca87cb7150dbf2bd79688bf7c04 Mon Sep 17 00:00:00 2001 From: ale Date: Sat, 16 Aug 2025 23:59:05 +0200 Subject: [PATCH] trust proxy Signed-off-by: ale --- DEPLOYMENT.md | 268 ++++++++++++++++++++++++++++++++++++ next.config.mjs | 27 +++- nginx.conf | 167 ++++++++++++++++++++++ src/app/api/ping/route.js | 18 ++- src/app/api/status/route.js | 16 ++- src/lib/proxy-utils.js | 129 +++++++++++++++++ src/middleware.js | 26 +++- 7 files changed, 638 insertions(+), 13 deletions(-) create mode 100644 DEPLOYMENT.md create mode 100644 nginx.conf create mode 100644 src/lib/proxy-utils.js diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..44aa6f7 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,268 @@ +# Deployment con Nginx - API Ping Service + +Esta guía explica cómo configurar nginx como proxy reverso para el servicio de ping, asegurando que se obtenga correctamente la IP real de los clientes. + +## 🔧 Configuración de Nginx + +### 1. Configuración Básica + +```nginx +server { + listen 80; + server_name your-domain.com; + + # Configuración para obtener IP real del cliente + real_ip_header X-Forwarded-For; + real_ip_recursive on; + + # IPs confiables (ajustar según tu infraestructura) + set_real_ip_from 10.0.0.0/8; + set_real_ip_from 172.16.0.0/12; + set_real_ip_from 192.168.0.0/16; + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_cache_bypass $http_upgrade; + } +} +``` + +### 2. Rate Limiting en Nginx + +```nginx +# En el bloque http {} +http { + # Zona de rate limiting para API general + limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m; + + # Zona específica para ping (más restrictiva) + limit_req_zone $binary_remote_addr zone=ping:10m rate=10r/m; + + # Rate limiting por IP real (después del proxy) + limit_req_zone $realip_remote_addr zone=real_ip:10m rate=10r/m; +} + +# En el bloque server {} +server { + location /api/ping { + limit_req zone=ping burst=5 nodelay; + proxy_pass http://127.0.0.1:3000; + # ... otros headers de proxy + } +} +``` + +### 3. SSL/HTTPS (Recomendado) + +```nginx +server { + listen 443 ssl http2; + ssl_certificate /path/to/certificate.pem; + ssl_certificate_key /path/to/private.key; + + # Headers de seguridad + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + + # ... resto de la configuración +} +``` + +## 🌐 Configuración para Cloudflare + +Si usas Cloudflare, agrega estas IPs confiables: + +```nginx +# IPs de Cloudflare (actualizar periódicamente) +set_real_ip_from 173.245.48.0/20; +set_real_ip_from 103.21.244.0/22; +set_real_ip_from 103.22.200.0/22; +# ... (ver archivo nginx.conf completo) + +# Headers específicos para Cloudflare +real_ip_header CF-Connecting-IP; +``` + +## 🚀 Pasos de Deployment + +### 1. Preparar la Aplicación + +```bash +# Compilar para producción +npm run build + +# Iniciar en modo producción +npm start + +# O usar PM2 para gestión de procesos +npm install -g pm2 +pm2 start npm --name "api-ping" -- start +pm2 save +pm2 startup +``` + +### 2. Configurar Nginx + +```bash +# Copiar configuración +sudo cp nginx.conf /etc/nginx/sites-available/api-ping +sudo ln -s /etc/nginx/sites-available/api-ping /etc/nginx/sites-enabled/ + +# Verificar configuración +sudo nginx -t + +# Recargar nginx +sudo systemctl reload nginx +``` + +### 3. Variables de Entorno + +```bash +# .env.production +NODE_ENV=production +TRUST_PROXY=true +RATE_LIMIT_MAX=10 +RATE_LIMIT_WINDOW_MS=60000 +``` + +## 🔍 Verificación del Setup + +### 1. Verificar Headers de Proxy + +```bash +# Verificar que nginx está pasando los headers correctos +curl -H "X-Forwarded-For: 203.0.113.1" http://your-domain.com/api/status +``` + +### 2. Test de Rate Limiting + +```bash +# Script para probar rate limiting +for i in {1..15}; do + echo "Request $i:" + curl -s http://your-domain.com/api/ping \ + -H "Content-Type: application/json" \ + -d '{"target":"8.8.8.8","count":1}' \ + | jq '.rateLimit // .error' + sleep 1 +done +``` + +### 3. Verificar IP del Cliente + +El endpoint `/api/status` ahora incluye información de debugging: + +```json +{ + "clientInfo": { + "ip": "203.0.113.1", + "isProxied": true, + "userAgent": "curl/7.68.0", + "proxyHeaders": { + "x-forwarded-for": "203.0.113.1", + "x-real-ip": "203.0.113.1" + } + } +} +``` + +## 🛡️ Seguridad Adicional + +### 1. Fail2ban para Protección DDoS + +```ini +# /etc/fail2ban/jail.local +[api-ping] +enabled = true +port = http,https +filter = api-ping +logpath = /var/log/nginx/api-ping-access.log +maxretry = 20 +bantime = 3600 +findtime = 300 +``` + +### 2. Monitoreo de Logs + +```bash +# Monitorear requests sospechosos +tail -f /var/log/nginx/api-ping-access.log | grep -E "(429|5[0-9]{2})" + +# Analizar IPs con más requests +awk '{print $1}' /var/log/nginx/api-ping-access.log | sort | uniq -c | sort -nr | head -10 +``` + +## 📊 Monitoreo y Métricas + +### 1. Nginx Status + +```nginx +location /nginx_status { + stub_status on; + access_log off; + allow 127.0.0.1; + deny all; +} +``` + +### 2. Logs Estructurados + +```nginx +log_format api_ping '$remote_addr - $remote_user [$time_local] ' + '"$request" $status $body_bytes_sent ' + '"$http_referer" "$http_user_agent" ' + '"$http_x_forwarded_for" "$http_x_real_ip" ' + '$request_time $upstream_response_time'; + +access_log /var/log/nginx/api-ping-access.log api_ping; +``` + +## 🔧 Troubleshooting + +### Problema: IP incorrecta + +```bash +# Verificar headers que llegan a la aplicación +curl -H "X-Debug: true" http://your-domain.com/api/status + +# Verificar configuración de nginx +sudo nginx -T | grep -A 10 -B 10 real_ip +``` + +### Problema: Rate limiting no funciona + +```bash +# Verificar que la IP se está obteniendo correctamente +# La aplicación debe usar la IP real, no la del proxy (127.0.0.1) +``` + +### Problema: CORS + +```bash +# Verificar headers CORS +curl -H "Origin: http://example.com" -H "Access-Control-Request-Method: POST" \ + -X OPTIONS http://your-domain.com/api/ping -v +``` + +## 📝 Checklist de Deployment + +- [ ] Aplicación compilada para producción +- [ ] PM2 o similar configurado para gestión de procesos +- [ ] Nginx configurado con proxy_pass +- [ ] Headers X-Forwarded-* configurados +- [ ] real_ip_header configurado +- [ ] IPs confiables (set_real_ip_from) configuradas +- [ ] Rate limiting configurado en nginx (opcional) +- [ ] SSL/HTTPS configurado +- [ ] Headers de seguridad configurados +- [ ] Logs configurados +- [ ] Monitoring configurado +- [ ] Backup y recovery plan preparado + +Con esta configuración, el servicio obtendrá correctamente la IP real de los clientes incluso cuando esté detrás de nginx, Cloudflare u otros proxies. 🎉 diff --git a/next.config.mjs b/next.config.mjs index 4678774..3a2591a 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,4 +1,29 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + // Configuración para proxy reverso (nginx) + async headers() { + return [ + { + source: '/api/:path*', + headers: [ + { + key: 'X-Forwarded-Proto', + value: 'https', + }, + ], + }, + ]; + }, + + // Configuración experimental para mejorar el manejo de headers + experimental: { + serverComponentsExternalPackages: ['ping'], + }, + + // Configuración de red + env: { + TRUST_PROXY: 'true', + }, +}; export default nextConfig; diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..e3c825a --- /dev/null +++ b/nginx.conf @@ -0,0 +1,167 @@ +# Configuración de nginx para API Ping Service +# Este archivo debe ser adaptado según tu configuración específica + +server { + listen 80; + listen [::]:80; + server_name your-domain.com; # Cambiar por tu dominio + + # Redirección HTTPS (recomendado para producción) + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name your-domain.com; # Cambiar por tu dominio + + # Configuración SSL (ajustar rutas según tu setup) + ssl_certificate /path/to/your/certificate.pem; + ssl_certificate_key /path/to/your/private.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384; + + # Headers de seguridad + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';" always; + + # Configuración para obtener la IP real del cliente + real_ip_header X-Forwarded-For; + real_ip_recursive on; + + # IPs confiables (ajustar según tu infraestructura) + # Cloudflare IPs (si usas Cloudflare) + set_real_ip_from 173.245.48.0/20; + set_real_ip_from 103.21.244.0/22; + set_real_ip_from 103.22.200.0/22; + set_real_ip_from 103.31.4.0/22; + set_real_ip_from 141.101.64.0/18; + set_real_ip_from 108.162.192.0/18; + set_real_ip_from 190.93.240.0/20; + set_real_ip_from 188.114.96.0/20; + set_real_ip_from 197.234.240.0/22; + set_real_ip_from 198.41.128.0/17; + set_real_ip_from 162.158.0.0/15; + set_real_ip_from 104.16.0.0/13; + set_real_ip_from 104.24.0.0/14; + set_real_ip_from 172.64.0.0/13; + set_real_ip_from 131.0.72.0/22; + set_real_ip_from 2400:cb00::/32; + set_real_ip_from 2606:4700::/32; + set_real_ip_from 2803:f800::/32; + set_real_ip_from 2405:b500::/32; + set_real_ip_from 2405:8100::/32; + set_real_ip_from 2a06:98c0::/29; + set_real_ip_from 2c0f:f248::/32; + + # IPs locales/privadas (ajustar según tu red) + set_real_ip_from 10.0.0.0/8; + set_real_ip_from 172.16.0.0/12; + set_real_ip_from 192.168.0.0/16; + + # Proxy a Next.js + location / { + proxy_pass http://127.0.0.1:3000; # Ajustar puerto si es necesario + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_cache_bypass $http_upgrade; + + # Timeouts + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # Configuración específica para API endpoints + location /api/ { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + # Rate limiting adicional en nginx (opcional) + limit_req zone=api burst=20 nodelay; + + # Headers CORS para API + add_header Access-Control-Allow-Origin "*" always; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; + add_header Access-Control-Allow-Headers "Content-Type, Authorization" always; + + # Manejar preflight requests + if ($request_method = OPTIONS) { + add_header Access-Control-Allow-Origin "*"; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; + add_header Access-Control-Allow-Headers "Content-Type, Authorization"; + add_header Content-Length 0; + add_header Content-Type text/plain; + return 200; + } + } + + # Rate limiting para proteger la API + location /api/ping { + limit_req zone=ping burst=5 nodelay; + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Archivos estáticos con cache + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $host; + expires 1y; + add_header Cache-Control "public, immutable"; + add_header X-Cache-Status "STATIC"; + } + + # Logs + access_log /var/log/nginx/api-ping-access.log; + error_log /var/log/nginx/api-ping-error.log; +} + +# Rate limiting zones (definir en http block) +# Agregar estas líneas al bloque http {} en nginx.conf: +# +# http { +# # Rate limiting para API general +# limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m; +# +# # Rate limiting específico para ping (más restrictivo) +# limit_req_zone $binary_remote_addr zone=ping:10m rate=10r/m; +# +# # Rate limiting por IP real (considerando proxy) +# limit_req_zone $realip_remote_addr zone=real_ip:10m rate=10r/m; +# } + +# Configuración adicional para development/testing +# Archivo separado: nginx-dev.conf +# +# server { +# listen 80; +# server_name localhost; +# +# location / { +# proxy_pass http://127.0.0.1:3000; +# proxy_set_header Host $host; +# proxy_set_header X-Real-IP $remote_addr; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +# proxy_set_header X-Forwarded-Proto $scheme; +# } +# } diff --git a/src/app/api/ping/route.js b/src/app/api/ping/route.js index 84f2353..fb407ef 100644 --- a/src/app/api/ping/route.js +++ b/src/app/api/ping/route.js @@ -3,17 +3,25 @@ import { NextResponse } from 'next/server'; import { RateLimiter } from '../../../lib/rate-limiter'; import { validateIP } from '../../../lib/validators'; import { PING_CONFIG } from '../../../lib/config.js'; +import { getClientIP, getProxyInfo } from '../../../lib/proxy-utils.js'; // Crear instancia del rate limiter const rateLimiter = new RateLimiter(); export async function POST(request) { try { - // Obtener IP del cliente para rate limiting - const forwarded = request.headers.get('x-forwarded-for'); - const clientIP = forwarded - ? forwarded.split(',')[0].trim() - : request.headers.get('x-real-ip') || 'unknown'; + // Obtener IP del cliente considerando proxies (nginx, cloudflare, etc.) + const clientIP = getClientIP(request); + const proxyInfo = getProxyInfo(request); + + // Log para debugging en desarrollo + if (PING_CONFIG.DEBUG.LOG_REQUESTS) { + console.log('Ping request from:', { + clientIP, + isProxied: proxyInfo.isProxied, + proxyHeaders: proxyInfo.proxyHeaders + }); + } // Verificar rate limit const rateLimitResult = await rateLimiter.checkLimit(clientIP); diff --git a/src/app/api/status/route.js b/src/app/api/status/route.js index 914919a..e40b97a 100644 --- a/src/app/api/status/route.js +++ b/src/app/api/status/route.js @@ -1,16 +1,15 @@ import { NextResponse } from 'next/server'; import { RateLimiter } from '../../../lib/rate-limiter'; +import { getClientIP, getProxyInfo } from '../../../lib/proxy-utils.js'; // Crear instancia del rate limiter const rateLimiter = new RateLimiter(); export async function GET(request) { try { - // Obtener IP del cliente - const forwarded = request.headers.get('x-forwarded-for'); - const clientIP = forwarded - ? forwarded.split(',')[0].trim() - : request.headers.get('x-real-ip') || 'unknown'; + // Obtener IP del cliente considerando proxies + const clientIP = getClientIP(request); + const proxyInfo = getProxyInfo(request); // Obtener estadísticas del rate limiter para este cliente const rateLimitStats = rateLimiter.getStats(clientIP); @@ -23,7 +22,12 @@ export async function GET(request) { uptime: process.uptime(), clientInfo: { ip: clientIP, - rateLimit: rateLimitStats + isProxied: proxyInfo.isProxied, + userAgent: proxyInfo.userAgent, + rateLimit: rateLimitStats, + ...(process.env.NODE_ENV === 'development' && { + proxyHeaders: proxyInfo.proxyHeaders + }) }, features: { maxPingsPerRequest: 10, diff --git a/src/lib/proxy-utils.js b/src/lib/proxy-utils.js new file mode 100644 index 0000000..65b048e --- /dev/null +++ b/src/lib/proxy-utils.js @@ -0,0 +1,129 @@ +/** + * Extrae la IP real del cliente considerando proxies como nginx + * @param {Request} request - La request de Next.js + * @returns {string} La IP del cliente + */ +export function getClientIP(request) { + // Headers que pueden contener la IP real del cliente + const forwardedFor = request.headers.get('x-forwarded-for'); + const realIP = request.headers.get('x-real-ip'); + const cfConnectingIP = request.headers.get('cf-connecting-ip'); // Cloudflare + const trueClientIP = request.headers.get('true-client-ip'); // Cloudflare Enterprise + const xClientIP = request.headers.get('x-client-ip'); + const xForwardedHost = request.headers.get('x-forwarded-host'); + + // Prioridad de headers (más confiables primero) + let clientIP = null; + + // 1. Cloudflare headers (muy confiables) + if (trueClientIP) { + clientIP = trueClientIP.trim(); + } else if (cfConnectingIP) { + clientIP = cfConnectingIP.trim(); + } + // 2. X-Real-IP (nginx real_ip_module) + else if (realIP) { + clientIP = realIP.trim(); + } + // 3. X-Forwarded-For (puede contener múltiples IPs) + else if (forwardedFor) { + // X-Forwarded-For puede contener múltiples IPs separadas por comas + // La primera es la IP original del cliente + const ips = forwardedFor.split(',').map(ip => ip.trim()); + clientIP = ips[0]; + } + // 4. Otros headers menos comunes + else if (xClientIP) { + clientIP = xClientIP.trim(); + } + + // Validar que la IP obtenida sea válida + if (clientIP && isValidIP(clientIP)) { + return clientIP; + } + + // Fallback: usar una IP por defecto + return '127.0.0.1'; +} + +/** + * Valida si una cadena es una IP válida (IPv4 o IPv6) + * @param {string} ip - La IP a validar + * @returns {boolean} True si es válida + */ +function isValidIP(ip) { + // Regex básico para IPv4 + const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/; + // Regex básico para IPv6 + const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$/; + + if (ipv4Regex.test(ip)) { + // Validar rangos de IPv4 (0-255) + const parts = ip.split('.'); + return parts.every(part => { + const num = parseInt(part, 10); + return num >= 0 && num <= 255; + }); + } + + if (ipv6Regex.test(ip) || ip === '::1' || ip.startsWith('::ffff:')) { + return true; + } + + return false; +} + +/** + * Obtiene información detallada del proxy y cliente + * @param {Request} request - La request de Next.js + * @returns {object} Información del cliente y proxy + */ +export function getProxyInfo(request) { + const headers = {}; + + // Recopilar todos los headers relevantes para debugging + const proxyHeaders = [ + 'x-forwarded-for', + 'x-real-ip', + 'x-forwarded-proto', + 'x-forwarded-host', + 'x-forwarded-port', + 'cf-connecting-ip', + 'true-client-ip', + 'x-client-ip', + 'x-cluster-client-ip', + 'forwarded' + ]; + + proxyHeaders.forEach(header => { + const value = request.headers.get(header); + if (value) { + headers[header] = value; + } + }); + + const clientIP = getClientIP(request); + + return { + clientIP, + proxyHeaders: headers, + isProxied: Object.keys(headers).length > 0, + userAgent: request.headers.get('user-agent') || 'unknown' + }; +} + +/** + * Middleware helper para configurar trust proxy en desarrollo + * @param {Request} request + * @returns {object} Configuración de trust + */ +export function getTrustProxyConfig(request) { + const isProduction = process.env.NODE_ENV === 'production'; + const trustProxy = process.env.TRUST_PROXY === 'true'; + + return { + trustProxy: isProduction || trustProxy, + clientIP: getClientIP(request), + environment: process.env.NODE_ENV || 'development' + }; +} diff --git a/src/middleware.js b/src/middleware.js index 71664aa..0aeb530 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -1,9 +1,13 @@ import { NextResponse } from 'next/server'; +import { getClientIP } from './lib/proxy-utils.js'; export function middleware(request) { // Crear la respuesta const response = NextResponse.next(); + // Obtener IP real del cliente + const clientIP = getClientIP(request); + // Agregar headers de seguridad response.headers.set('X-Content-Type-Options', 'nosniff'); response.headers.set('X-Frame-Options', 'DENY'); @@ -12,16 +16,36 @@ export function middleware(request) { // Headers específicos para API if (request.nextUrl.pathname.startsWith('/api/')) { + // CORS headers response.headers.set('Access-Control-Allow-Origin', '*'); response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); response.headers.set('Access-Control-Allow-Headers', 'Content-Type'); + // Headers para debugging de proxy (solo en desarrollo) + if (process.env.NODE_ENV === 'development') { + response.headers.set('X-Debug-Client-IP', clientIP); + response.headers.set('X-Debug-Original-IP', request.ip || 'unknown'); + } + // Manejar preflight requests if (request.method === 'OPTIONS') { - return new Response(null, { status: 200, headers: response.headers }); + return new Response(null, { + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Max-Age': '86400', // 24 horas + } + }); } } + // Headers para contenido estático + if (request.nextUrl.pathname.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg)$/)) { + response.headers.set('Cache-Control', 'public, max-age=31536000, immutable'); + } + return response; }