commit 42b5ef099d9c0e8e99d84382366ab49ff7df69a7 Author: Tu Nombre Date: Fri Sep 19 14:31:14 2025 +0200 Nuevo mensaje del commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..455c641 --- /dev/null +++ b/.gitignore @@ -0,0 +1,68 @@ +# Ignorar logs +logs/ +*.log + +# Ignorar certificados reales (por seguridad) +certs/*.crt +certs/*.key +certs/*.pem + +# Pero mantener el README de certificados +!certs/README.md + +# Ignorar archivos temporales +*.tmp +*.swp +*.swo +*~ + +# Ignorar archivos de Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Ignorar archivos de entorno +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Ignorar archivos de IDE +.vscode/ +.idea/ +*.sublime-* + +# Ignorar archivos de sistema +.DS_Store +Thumbs.db + +# Ignorar backups +*.backup +backup_* +*.tar.gz + +# Ignorar archivos de configuración personal +config/local_* +config/*.local.* \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..92c14f3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +FROM python:3.11-slim + +# Instalar dependencias del sistema +RUN apt-get update && apt-get install -y \ + openssh-client \ + ca-certificates \ + netcat-openbsd \ + curl \ + telnet \ + && rm -rf /var/lib/apt/lists/* + +# Crear directorio de trabajo +WORKDIR /app + +# Copiar requirements +COPY requirements.txt . + +# Instalar dependencias de Python +RUN pip install --no-cache-dir -r requirements.txt + +# Crear directorios necesarios +RUN mkdir -p /app/certs /app/config /app/logs /app/scripts + +# Copiar archivos de la aplicación +COPY src/ ./src/ +COPY config/ ./config/ +COPY scripts/ ./scripts/ + +# Crear usuario no-root +RUN useradd -m -u 1000 proxyuser && chown -R proxyuser:proxyuser /app +USER proxyuser + +# Exponer puerto para la gestión interna del proxy +EXPOSE 8080 + +# Comando por defecto +CMD ["python", "src/industrial_nat_manager.py"] \ No newline at end of file diff --git a/INDUSTRIAL_README.md b/INDUSTRIAL_README.md new file mode 100644 index 0000000..70a062d --- /dev/null +++ b/INDUSTRIAL_README.md @@ -0,0 +1,237 @@ +# Sistema NAT Industrial para Acceso a PLCs/SCADA + +## 🎯 **Arquitectura de Red** + +``` +PC2 (Remoto) → PC3 (91.99.210.72) → PC1 (WSL2+VPN) → PLCs/SCADA (10.1.33.x) + ↑ ↑ ↑ ↑ +ZeroTier/Internet SSH Tunnel Túnel Reverso Red Corporativa + Intermediario desde WSL2 (GlobalConnect VPN) +``` + +## 🏭 **Casos de Uso Industriales** + +- **VNC a PLCs** - Acceso gráfico remoto a pantallas HMI +- **Interfaces Web** - Configuración de dispositivos industriales +- **Modbus TCP** - Comunicación con controladores +- **SSH/Telnet** - Acceso terminal a equipos +- **Bases de datos** - Historiadores y sistemas SCADA + +## 🚀 **Instalación en PC1 (WSL2)** + +### 1. Configurar Clave SSH +```bash +# Copiar tu clave privada SSH +cp /ruta/a/tu/clave_privada certs/ssh_private_key +chmod 600 certs/ssh_private_key +``` + +### 2. Configurar Usuario SSH en PC3 +Editar `config/nat_config.yaml`: +```yaml +ssh_server: + host: "91.99.210.72" + user: "tu_usuario_ssh" # Cambiar aquí +``` + +### 3. Iniciar Sistema +```bash +./setup.sh +``` + +## 🖥️ **Uso desde PC2 (Cliente Remoto)** + +### Conexión Rápida a PLCs + +```bash +# Instalar cliente en PC2 +pip install requests + +# Conectar a PLC por VNC (asigna puerto automáticamente) +python nat_client.py plc 10.1.33.11 vnc --wait + +# Resultado: +# ✅ Conexión a PLC establecida! +# Acceso desde PC2: 91.99.210.72:9001 +# Servicio: VNC + +# Ahora desde PC2 conectar VNC a: 91.99.210.72:9001 +``` + +### Servicios Predefinidos + +```bash +# VNC (puerto 5900) +python nat_client.py plc 10.1.33.11 vnc + +# Interface Web (puerto 80) +python nat_client.py plc 10.1.33.11 web + +# Modbus TCP (puerto 502) +python nat_client.py plc 10.1.33.12 modbus + +# SSH al PLC (puerto 22) +python nat_client.py plc 10.1.33.13 ssh +``` + +### Conexión a Puerto Personalizado + +```bash +# Conectar a puerto específico +python nat_client.py connect 10.1.33.11 8080 --description "PLC Web Admin" + +# Puerto específico en PC3 +python nat_client.py add 10.1.33.11 1234 --external-port 9500 +``` + +### Ver Estado del Sistema + +```bash +# Estado completo +python nat_client.py status + +# Listar conexiones activas +python nat_client.py list +``` + +## 📊 **Ejemplos Prácticos** + +### Escenario 1: Acceso VNC a HMI +```bash +# Desde PC2 crear túnel +python nat_client.py plc 10.1.33.11 vnc --wait + +# Conectar VNC viewer a: 91.99.210.72:9001 +# ¡Ya tienes acceso al HMI como si estuvieras en la planta! +``` + +### Escenario 2: Configurar Múltiples PLCs +```bash +# PLC Principal - VNC +python nat_client.py plc 10.1.33.11 vnc + +# PLC Principal - Web +python nat_client.py plc 10.1.33.11 web + +# PLC Secundario - Modbus +python nat_client.py plc 10.1.33.12 modbus + +# Verificar conexiones +python nat_client.py list +``` + +### Escenario 3: Acceso a Historiador +```bash +# Base de datos del historiador +python nat_client.py connect 10.1.33.20 1433 --description "SQL Server Historiador" + +# Conectar desde PC2: 91.99.210.72:9XXX +``` + +## 🔧 **API REST para Automatización** + +```python +import requests + +# Crear conexión programáticamente +response = requests.post('http://91.99.210.72:8080/quick-connect', json={ + 'target_ip': '10.1.33.11', + 'target_port': 5900, + 'description': 'Acceso VNC automatizado' +}) + +connection = response.json() +print(f"Conectar VNC a: {connection['access_url']}") +``` + +## 🛡️ **Seguridad** + +- **Túneles SSH cifrados** - Todo el tráfico está protegido +- **Sin puertos abiertos en PC1** - Solo conexiones salientes +- **Acceso controlado** - Solo dispositivos autorizados via IP +- **Logs detallados** - Auditoría completa de conexiones + +## 🔍 **Monitoreo y Logs** + +```bash +# Ver logs en tiempo real +./scripts/manage_proxy.sh logs + +# Estado del sistema NAT +curl http://localhost:8080/status + +# Conexiones activas por PLC +python nat_client.py status | grep "10.1.33" +``` + +## 📱 **Gestión desde PC2** + +### Script de Conexión Rápida (Windows) +```batch +@echo off +echo Conectando a PLC Principal... +python nat_client.py plc 10.1.33.11 vnc --wait +echo. +echo ¡Listo! Conecta tu VNC viewer a: 91.99.210.72:9001 +pause +``` + +### PowerShell para Múltiples PLCs +```powershell +# Conectar a todos los PLCs de la línea de producción +$plcs = @("10.1.33.11", "10.1.33.12", "10.1.33.13") + +foreach ($plc in $plcs) { + Write-Host "Conectando a PLC $plc..." + python nat_client.py plc $plc vnc +} + +# Mostrar estado +python nat_client.py list +``` + +## 🚨 **Resolución de Problemas** + +### PC1 no puede conectar a PC3 +```bash +# Verificar clave SSH +ssh -i certs/ssh_private_key usuario@91.99.210.72 + +# Verificar conectividad +ping 91.99.210.72 +``` + +### PC2 no puede acceder al puerto +```bash +# Verificar que el túnel esté activo +python nat_client.py status + +# Probar conectividad a PC3 +telnet 91.99.210.72 9001 +``` + +### PLC no responde +```bash +# Desde PC1, verificar acceso al PLC +ping 10.1.33.11 +telnet 10.1.33.11 5900 +``` + +## 📋 **Puertos Comunes Industriales** + +| Servicio | Puerto | Descripción | +|----------|--------|-------------| +| VNC | 5900 | Acceso gráfico HMI | +| HTTP | 80 | Interface web PLC | +| HTTPS | 443 | Interface web segura | +| Modbus TCP | 502 | Comunicación Modbus | +| SSH | 22 | Terminal remoto | +| Telnet | 23 | Terminal (inseguro) | +| FTP | 21 | Transferencia archivos | +| SQL Server | 1433 | Base datos historiador | +| MySQL | 3306 | Base datos | +| OPC | 135 | OPC Classic | + +--- + +**¡Sistema listo!** Ahora PC2 puede acceder a cualquier dispositivo en la red corporativa como si estuviera físicamente conectado en la planta. \ No newline at end of file diff --git a/PC3_SETUP.md b/PC3_SETUP.md new file mode 100644 index 0000000..7d69bab --- /dev/null +++ b/PC3_SETUP.md @@ -0,0 +1,191 @@ +# Configuración de PC3 (Servidor Linux Intermediario) +# Instrucciones para configurar 91.99.210.72 + +## 🖥️ **Requisitos Mínimos en PC3** + +PC3 solo necesita ser un **servidor Linux estándar con SSH**. No requiere software especial. + +### **Software Necesario (probablemente ya instalado):** +- ✅ **SSH Server** (openssh-server) +- ✅ **Acceso de red** (puede recibir conexiones) + +## 🔧 **Verificación y Configuración** + +### **1. Verificar SSH Server** +```bash +# Verificar si SSH está instalado y ejecutándose +sudo systemctl status ssh +# o en sistemas más antiguos: +sudo service ssh status + +# Si no está instalado: +sudo apt update +sudo apt install openssh-server + +# Iniciar SSH si no está activo: +sudo systemctl start ssh +sudo systemctl enable ssh +``` + +### **2. Verificar Puerto SSH** +```bash +# Verificar que SSH escucha en puerto 22 +sudo netstat -tlnp | grep :22 +# o +sudo ss -tlnp | grep :22 + +# Debería mostrar algo como: +# tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN +``` + +### **3. Configurar Clave SSH (Método Recomendado)** +```bash +# En PC3, agregar tu clave pública SSH +mkdir -p ~/.ssh +chmod 700 ~/.ssh + +# Copiar tu clave pública al archivo authorized_keys +# (La clave pública correspondiente a la privada que tienes) +nano ~/.ssh/authorized_keys + +# Establecer permisos correctos +chmod 600 ~/.ssh/authorized_keys +``` + +### **4. Configuración SSH Opcional (para mayor seguridad)** +```bash +# Editar configuración SSH +sudo nano /etc/ssh/sshd_config + +# Configuraciones recomendadas: +Port 22 # Puerto SSH (puedes cambiarlo) +PermitRootLogin yes # Solo si usas root (no recomendado) +PubkeyAuthentication yes # Autenticación por clave pública +PasswordAuthentication no # Deshabilitar password (más seguro) +GatewayPorts yes # IMPORTANTE: Permitir túneles reversos +AllowTcpForwarding yes # IMPORTANTE: Permitir forwarding + +# Reiniciar SSH después de cambios +sudo systemctl restart ssh +``` + +## ⚠️ **Configuración CRÍTICA para Túneles Reversos** + +La configuración más importante es **GatewayPorts**: + +```bash +# En /etc/ssh/sshd_config debe estar: +GatewayPorts yes + +# Esto permite que los túneles SSH reversos sean accesibles +# desde cualquier IP, no solo localhost +``` + +### **Sin GatewayPorts:** +- Túneles solo accesibles desde localhost en PC3 +- PC2 NO puede conectar + +### **Con GatewayPorts yes:** +- Túneles accesibles desde cualquier IP +- PC2 SÍ puede conectar a 91.99.210.72:puerto + +## 🔥 **Firewall (si está habilitado)** + +```bash +# Verificar si hay firewall activo +sudo ufw status +# o +sudo iptables -L + +# Si ufw está activo, permitir SSH: +sudo ufw allow 22 + +# Permitir rango de puertos para túneles (9000-9999) +sudo ufw allow 9000:9999/tcp + +# Si usas iptables directamente: +sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT +sudo iptables -A INPUT -p tcp --dport 9000:9999 -j ACCEPT +``` + +## 🧪 **Verificar Configuración Desde PC1** + +Desde PC1 (WSL2), probar la conexión SSH: + +```bash +# Probar conexión SSH básica +ssh -i certs/ssh_private_key usuario@91.99.210.72 + +# Probar túnel SSH reverso +ssh -i certs/ssh_private_key -R 9999:localhost:8080 usuario@91.99.210.72 + +# Desde otra terminal en PC3, verificar que el puerto esté abierto: +netstat -tlnp | grep 9999 +``` + +## 📋 **Checklist de Configuración PC3** + +- [ ] ✅ SSH Server instalado y ejecutándose +- [ ] ✅ Puerto 22 abierto y accesible +- [ ] ✅ Tu clave pública en `~/.ssh/authorized_keys` +- [ ] ✅ `GatewayPorts yes` en `/etc/ssh/sshd_config` +- [ ] ✅ `AllowTcpForwarding yes` en SSH config +- [ ] ✅ Firewall permite puerto 22 y rango 9000-9999 +- [ ] ✅ Prueba de conexión SSH desde PC1 exitosa + +## 🎯 **Configuración Mínima Rápida** + +Si PC3 es un servidor Linux básico, estos comandos bastan: + +```bash +# Instalar SSH si no está +sudo apt update && sudo apt install -y openssh-server + +# Configurar SSH para túneles reversos +echo "GatewayPorts yes" | sudo tee -a /etc/ssh/sshd_config +echo "AllowTcpForwarding yes" | sudo tee -a /etc/ssh/sshd_config + +# Reiniciar SSH +sudo systemctl restart ssh + +# Permitir puertos en firewall +sudo ufw allow 22 +sudo ufw allow 9000:9999/tcp +``` + +## 🔍 **Verificación Final** + +Desde PC2 (o cualquier PC externa), verificar acceso: + +```bash +# Probar acceso SSH +ssh usuario@91.99.210.72 + +# Una vez que PC1 establezca túneles, probar acceso a puertos: +telnet 91.99.210.72 9001 +curl http://91.99.210.72:9001 +``` + +## 📞 **Soporte de Proveedores Cloud** + +Si PC3 está en la nube: + +### **AWS EC2:** +- Configurar Security Group para puerto 22 y 9000-9999 +- Usar Amazon Linux o Ubuntu + +### **Google Cloud:** +- Configurar reglas de firewall para puertos necesarios +- Usar Compute Engine con Ubuntu/Debian + +### **Digital Ocean:** +- Droplet básico con Ubuntu +- Configurar Cloud Firewall + +### **VPS Genérico:** +- Cualquier VPS Linux con acceso SSH +- Configurar según las instrucciones de arriba + +--- + +**En resumen:** PC3 solo necesita ser un servidor Linux con SSH configurado para túneles reversos. ¡La configuración es muy simple! \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e72f1d7 --- /dev/null +++ b/README.md @@ -0,0 +1,375 @@ +# Sistema NAT Industrial para Acceso a PLCs/SCADA + +Este proyecto crea un **sistema NAT dinámico** en WSL2 que permite a PC2 acceder a dispositivos PLC/SCADA en la red corporativa de PC1 a través de un servidor Linux intermediario (PC3). Soluciona las limitaciones de red de WSL2 y VPNs corporativas. + +## 🎯 **Arquitectura de Red Industrial** + +``` +PC2 (Remoto) → PC3 (91.99.210.72) → PC1 (WSL2+VPN) → PLCs/SCADA (10.1.33.x) + ↑ ↑ ↑ ↑ +ZeroTier/Internet SSH Tunnel Túnel Reverso Red Corporativa + Intermediario desde WSL2 (GlobalConnect VPN) +``` + +**Problema resuelto:** PC1 está en una VPN corporativa con acceso a PLCs pero no puede abrir puertos. PC2 necesita acceder a esos PLCs remotamente. + +## 🏭 **Casos de Uso Industriales** + +- **VNC a PLCs** - Acceso gráfico remoto a pantallas HMI +- **Interfaces Web** - Configuración de dispositivos industriales +- **Modbus TCP** - Comunicación con controladores +- **SSH/Telnet** - Acceso terminal a equipos industriales +- **Bases de datos** - Historiadores y sistemas SCADA +- **OPC/SCADA** - Protocolos industriales + +## ✨ **Características del Sistema** + +- ✅ **NAT Dinámico** - Conecta a cualquier IP:puerto sin configuración previa +- ✅ **Solo clave SSH privada** - No necesita certificados SSL complejos +- ✅ **Servicios industriales predefinidos** - VNC, Modbus, HTTP, SSH automáticos +- ✅ **Gestión desde PC2** - Control remoto completo via API REST +- ✅ **Sistema permanente** - Se ejecuta como servicio, auto-reinicio +- ✅ **Múltiples PLCs simultáneos** - Gestiona toda la planta industrial + +## 📁 Estructura del Proyecto + +``` +proxytcp/ +├── Dockerfile # Imagen del contenedor industrial +├── docker-compose.yml # Configuración de servicios +├── requirements.txt # Dependencias Python +├── src/ +│ └── industrial_nat_manager.py # Sistema NAT principal +├── config/ +│ └── nat_config.yaml # Configuración industrial +├── certs/ # Clave SSH privada +│ └── ssh_private_key # Tu clave SSH generada +├── scripts/ +│ ├── nat_client.py # Cliente para PC2 +│ ├── industrial_manager.sh # Gestión automatizada +│ └── generate_ssh_key.sh # Generador de claves +├── setup_permanent.sh # Configuración como servicio +└── logs/ # Logs del sistema +``` + +## 🚀 **Instalación Completa** + +### **Paso 1: Generar Clave SSH (PC1)** +```bash +# Generar nueva clave SSH específica +./scripts/generate_ssh_key.sh +``` + +### **Paso 2: Configurar PC3 (Servidor Intermediario)** +```bash +## 🚨 **Resolución de Problemas** + +### **Problemas Comunes** + +#### **1. Error de conexión SSH a PC3** +```bash +# Verificar clave SSH +ls -la certs/ssh_private_key +chmod 600 certs/ssh_private_key + +# Probar conexión manual +ssh -i certs/ssh_private_key miguefin@91.99.210.72 +``` + +#### **2. PLC no accesible desde PC2** +```bash +# Verificar túnel SSH activo +docker exec proxytcp_proxy_1 ps aux | grep ssh + +# Verificar configuración PC3 +ssh -i certs/ssh_private_key miguefin@91.99.210.72 "sudo netstat -tlnp | grep :9" +``` + +#### **3. Servicio no inicia automáticamente** +```bash +# Verificar servicio systemd +sudo systemctl status industrial-nat-manager + +# Ver logs del servicio +sudo journalctl -u industrial-nat-manager -f + +# Reiniciar servicio +sudo systemctl restart industrial-nat-manager +``` + +#### **4. Puertos ocupados** +```bash +# Verificar puertos en uso +docker exec proxytcp_proxy_1 netstat -tlnp + +# Limpiar conexiones +docker restart proxytcp_proxy_1 +``` + +### **Información de Red** + +``` +Flujo de Datos: +PC2 (Remoto) → PC3 (91.99.210.72) → PC1 (WSL2+VPN) → PLCs/SCADA (10.1.33.x) + +Puertos Dinámicos: 9000-9999 en PC3 +API de Control: Puerto 8080 en PC1 +``` + +## 📚 **Documentación Adicional** + +- **PC3_SETUP.md** - Configuración detallada del servidor intermediario +- **INDUSTRIAL_README.md** - Guía específica para uso industrial +- **config/nat_config.yaml** - Referencia de configuración completa + +## 🤝 **Soporte** + +Este sistema está diseñado para entornos industriales que requieren acceso remoto a PLCs y sistemas SCADA a través de limitaciones de red corporativa. + +**Casos de uso típicos:** +- Monitoreo remoto de plantas industriales +- Mantenimiento de equipos desde ubicaciones remotas +- Acceso a HMI/SCADA sin VPN corporativa +- Gestión de múltiples PLCs simultáneamente + +--- + +🏭 **Sistema NAT Industrial para Acceso Remoto a PLCs/SCADA** 🏭 +``` + +### **Paso 3: Configurar Sistema Permanente (PC1)** +```bash +# Instalar como servicio permanente +./setup_permanent.sh + +# ¡El sistema queda funcionando automáticamente! +``` + +## � **Uso del Sistema** + +### **Desde PC2 (Remoto) - Acceso Industrial** + +```bash +# Copiar el cliente a PC2 +scp nat_client.py pc2@ip.del.pc2:/ruta/destino/ + +# En PC2, conectar a PLCs usando servicios predefinidos: + +# 1. Conectar a PLC via VNC (visualización) +python nat_client.py plc 10.1.33.11 vnc + +# 2. Conectar a PLC via Modbus TCP (datos) +python nat_client.py plc 10.1.33.11 modbus + +# 3. Conectar a interfaz web del PLC +python nat_client.py plc 10.1.33.11 http + +# 4. Acceso SSH a dispositivo industrial +python nat_client.py plc 10.1.33.15 ssh + +# 5. Conexión personalizada +python nat_client.py connect 10.1.33.20 8080 --name "Servidor_SCADA" +``` + +### **Gestión Avanzada (PC1)** + +```bash +# Ver estado del sistema +docker exec proxytcp_proxy_1 python -c " +import aiohttp, asyncio +async def status(): + async with aiohttp.ClientSession() as session: + async with session.get('http://localhost:8080/status') as resp: + print(await resp.json()) +asyncio.run(status()) +" + +# Gestión interactiva +./scripts/industrial_manager.sh + +# Ver logs del sistema +docker logs proxytcp_proxy_1 -f +``` + +## 🔧 **Configuración Industrial** + +### **Archivo de Configuración (`config/nat_config.yaml`)** + +```yaml +# Configuración del servidor PC3 +ssh_server: + host: "91.99.210.72" + port: 22 + user: "miguefin" + key_file: "/app/certs/ssh_private_key" + +# Servicios industriales predefinidos +services: + vnc: + port: 5900 + description: "Acceso remoto a pantallas HMI" + modbus: + port: 502 + description: "Protocolo Modbus TCP" + http: + port: 80 + description: "Interfaces web de dispositivos" + ssh: + port: 22 + description: "Acceso SSH a dispositivos" + +# Configuración de puertos dinámicos +nat: + port_range: [9000, 9999] + bind_host: "0.0.0.0" +``` + +## 🔧 Tipos de Servicios Disponibles + +### 1. Servicio HTTP +Responde con JSON y información de la conexión: +```bash +# En el contenedor Docker expone puerto 3000 +# En el servidor Linux se accede por puerto 3000 +curl http://91.99.210.72:3000 +``` + +### 2. Servicio Echo +Útil para pruebas de conectividad: +```bash +# Usando telnet o netcat +echo "Hello World" | nc 91.99.210.72 7000 +``` + +### 3. Servicios Personalizados +Puedes crear tus propios servicios modificando `ssh_proxy_manager.py` + +## 💡 Casos de Uso Comunes + +### 1. API de Desarrollo +```bash +# Exponer API del contenedor al mundo +./scripts/manage_proxy.sh add 8000 8000 +# Acceso: http://91.99.210.72:8000 +``` + +### 2. Servicio de Pruebas +```bash +# Servicio echo para verificar conectividad +./scripts/manage_proxy.sh add 9999 9999 +# Prueba: echo "test" | nc 91.99.210.72 9999 +``` + +### 3. Múltiples Servicios +```bash +# Web frontend +./scripts/manage_proxy.sh add 3000 3000 + +# API backend +./scripts/manage_proxy.sh add 8000 8000 + +# WebSocket +./scripts/manage_proxy.sh add 9000 9000 +``` + +## 🐛 Resolución de Problemas + +### El contenedor no inicia +```bash +# Verificar logs +docker logs tcp-proxy-container + +# Verificar certificados +ls -la certs/ +``` + +### Error de conexión SSL +- Verifica que los certificados son válidos +- Confirma que el servidor remoto acepta tu certificado +- Revisa los logs para detalles del error + +### Puerto ya en uso +```bash +# Ver qué proceso usa el puerto +sudo netstat -tulpn | grep :PUERTO + +# Eliminar proxy existente +./scripts/manage_proxy.sh remove PUERTO +``` + +### Proxy no funciona +```bash +# Verificar estado +./scripts/manage_proxy.sh status + +# Probar conexión manual +telnet localhost PUERTO_LOCAL +``` + +## 🔐 Seguridad + +- Los certificados se montan como solo lectura +- El contenedor ejecuta con usuario no-root +- Las conexiones usan SSL/TLS end-to-end +- Los logs no registran datos sensibles + +## 📊 Monitoreo + +### Verificar Conexiones Activas +```bash +# Estado general +./scripts/manage_proxy.sh status + +# Conexiones detalladas +docker exec tcp-proxy-container netstat -an +``` + +### Métricas de Uso +```python +import requests + +status = requests.get('http://localhost:8080/status').json() +print(f"Total conexiones: {status['total_connections']}") + +for proxy in status['proxies']: + print(f"Puerto {proxy['local_port']}: {proxy['connections']} conexiones activas") +``` + +## 🚦 Scripts de Automatización + +### Script de Backup de Configuración +```bash +#!/bin/bash +# backup_config.sh +tar -czf "backup_$(date +%Y%m%d_%H%M%S).tar.gz" config/ certs/ +``` + +### Script de Monitoreo +```python +#!/usr/bin/env python3 +# monitor.py +import time +import requests +from datetime import datetime + +while True: + try: + status = requests.get('http://localhost:8080/status', timeout=5).json() + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + print(f"[{timestamp}] Proxies: {len(status['proxies'])}, Conexiones: {status['total_connections']}") + except Exception as e: + print(f"Error: {e}") + time.sleep(30) +``` + +## 📝 Licencia + +Este proyecto es de uso libre para fines personales y educativos. + +## 🤝 Contribuciones + +Para reportar problemas o sugerir mejoras, crea un issue en el repositorio del proyecto. + +--- + +**¡Listo para usar!** El sistema está configurado para conectar automáticamente con tu servidor Linux usando los certificados proporcionados. \ No newline at end of file diff --git a/certs/README.md b/certs/README.md new file mode 100644 index 0000000..832f7cd --- /dev/null +++ b/certs/README.md @@ -0,0 +1,24 @@ +# Configuración de clave SSH privada +# Solo necesitas tu clave privada SSH para conectar al servidor Linux + +# INSTRUCCIONES: +# 1. Coloca tu clave privada SSH en: /app/certs/ssh_private_key +# 2. Asegúrate de que los permisos sean correctos: +# chmod 600 ssh_private_key + +# FLUJO DE CONEXIÓN: +# PC Externa -> Servidor Linux (91.99.210.72) -> WSL2 Docker (túnel SSH reverso) +# +# El servidor Linux actúa como intermediario usando SSH tunneling +# ya que no puede escuchar puertos directamente + +# EJEMPLO DE CLAVE PRIVADA: +# La clave privada SSH debería verse como: +# -----BEGIN OPENSSH PRIVATE KEY----- +# b3BlbnNzaC1rZXktdjEAAAAA... +# -----END OPENSSH PRIVATE KEY----- + +# O formato RSA: +# -----BEGIN RSA PRIVATE KEY----- +# MIIEpAIBAAKCAQEA... +# -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/certs/ssh_private_key b/certs/ssh_private_key new file mode 100644 index 0000000..a139107 --- /dev/null +++ b/certs/ssh_private_key @@ -0,0 +1,50 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAgEApV3rhgNXNjZImX0DEyOKYC+lxCnA/QGVW130Z8d7pkKTPI2qQ9e5 +mEUs2rHF9A6jov0MYU1WJiWHteRI6pcbYgcyPJGm4FUIMkz39zRI3o99gMUpVMVNjaCl+7 +C29nIlCW1HhqL93p5OQmEb0SJhsuqoCa5i/5zPGPLKVhPBIc1cWfqaMoiQ0o3clRIpafxs +LgzdErq3wKeX/FiF96YEZjjPNQg26pBPIcAylESrSc7O4WdE51nY+ttqC02T517sP3CAO0 +mzBJSTt0DtbP4ovYRNCMkPhj8U0Jvxt+fV97vTmoa/RYEzsoQOGfYqOa9NoUrQXkhCtve0 +iHHI2M6prRlZfmMJFXOjbQ2w+19n6G4B3QNsehqDl+Fr5GMi7faxs1/VduvgyKnn05LE92 +jaClWNzCXMbF+BMkE958V7sUAxYxEYb3j1dGMetogelhsEvhA1VPTrx508CVhrDsp2viX5 ++f3Nhxs8KcYIoiNCyzf+LR+SAhC7zT2yB6hHy9AJk2Dm/AsRO7+Ro59d/I2+ENBkIXRhmn +LSonN8iL2M0gIwhQPiAtSklXNDyfnLkPm2Wh/5eXayz563hQz0Xg3UuG8wKE/CatNXoCUW +Bz2idi/DkvHnLaKwqo6L5lD6em4oAVGMuV9c/9niO4HlUDxuCYBiatkBSXRJOeb/cHXtC3 +UAAAdgN9mowDfZqMAAAAAHc3NoLXJzYQAAAgEApV3rhgNXNjZImX0DEyOKYC+lxCnA/QGV +W130Z8d7pkKTPI2qQ9e5mEUs2rHF9A6jov0MYU1WJiWHteRI6pcbYgcyPJGm4FUIMkz39z +RI3o99gMUpVMVNjaCl+7C29nIlCW1HhqL93p5OQmEb0SJhsuqoCa5i/5zPGPLKVhPBIc1c +WfqaMoiQ0o3clRIpafxsLgzdErq3wKeX/FiF96YEZjjPNQg26pBPIcAylESrSc7O4WdE51 +nY+ttqC02T517sP3CAO0mzBJSTt0DtbP4ovYRNCMkPhj8U0Jvxt+fV97vTmoa/RYEzsoQO +GfYqOa9NoUrQXkhCtve0iHHI2M6prRlZfmMJFXOjbQ2w+19n6G4B3QNsehqDl+Fr5GMi7f +axs1/VduvgyKnn05LE92jaClWNzCXMbF+BMkE958V7sUAxYxEYb3j1dGMetogelhsEvhA1 +VPTrx508CVhrDsp2viX5+f3Nhxs8KcYIoiNCyzf+LR+SAhC7zT2yB6hHy9AJk2Dm/AsRO7 ++Ro59d/I2+ENBkIXRhmnLSonN8iL2M0gIwhQPiAtSklXNDyfnLkPm2Wh/5eXayz563hQz0 +Xg3UuG8wKE/CatNXoCUWBz2idi/DkvHnLaKwqo6L5lD6em4oAVGMuV9c/9niO4HlUDxuCY +BiatkBSXRJOeb/cHXtC3UAAAADAQABAAACAAGWVnIMAUMLTKSYHNaxVv0H3Nmc0S+mG33y +RHi97/ultKSWgB2HfU5PG0UpHJGN/DebbrCjZz67/WK7UEQO+b7cSB9Pm/0X8j/YJpXBzU +SnnQiGolgtAaRo0ZcKHr0PiOssS5no+0wiLSjTDLGNi/sFghUZTnV0igKL2AtxKplaT4Pe +LvgDtA71UiC/+SCwwQTeVj19bp4whznimDyX8RkrFYZ4iV00gxhVMDCyPQoORff6mToQQU +Mp80lfyZoYeoBV+9q11FXoeC3m3cokevvtgGpjn0Mswu4iq3Rv5JYGM6iBmRkZcNQ9xlkT +WHUclkxRSJE5gW+gqCPnVbDXdCz2pxcgOSj7Br+QUXOw4Fr3IjYwzVEQfJNrg4r4H8GY4Z +XzsgTgldoX7v2VgAM6ftZGMzsRgGiWfqJiOKrFyuZhfVH0K3w36aoZnmsQrJaqpo/6f+pL +2FUsWlY9DrUMjvZyBcVklfV4qqxquWnyy9gy80Y31Hny7k/CadQgsI4LGEuJFhZrkvvxEH +1nuyznDgqcFHmUUTw/TgXiDynIT4MJGYR/R4xKjr2z6Bw6O++7vNS9S4S4aGH2biZzBiGE +YiY1u2oM3/c+jQwqVKynIaHtHLAVA21MMfux5aOu0mV+b7KT816Ds5Llq1X6ithU5ROJe6 +K2TqK1ZNLi2YuaO5CJAAABAFORWcwf2OXlVdVMyv0GmyY89KicEWs/S33g2amoEv031FFX +98r5DR+TnzQV1GxQgLO7nKRZS27AKL/eRmLebJV1tnpFJsEYfHy0Kv8oyObq7Ld+EyQyPb +K1a1RTFTqZziR2yhUjkOaXY22A1Ub/7LI2GY1YUAhnUbZ/gQoFIdwoUkT4o9YvxucEKyaE +3dZSP6bwlUhOQpcE6JouD66gjPSBfSVDpSvehQ7UR6ov0sdQtl9PqOOPOSnxnwqY4O7J+y +H7ASLzoywGG4o+Jz88tucNBnsE9vfAzop+EWy0b1IkSne1WQcAx6sd303AkE3uy26EegdA +WMJYvycdANWU4wIAAAEBAOCzrlBK8oRk7hTRyee/3n8a/qXu/iJQWlEGc1jmwMubPIwYce +728ibxM1sLlG+122KvmvR5j4HAFejW7nimFlETGtRuxPTm+MZHUP8jvwq0cR9ufuS/wVzJ +R3Q/flkltFEvN/VkLJQkPNyhXbBudY21+5eaRZYswsOLGUtwJMsSa4WiKfG75YxAOsBEKx +r/N1RsfgGM9Z6elhaDcdKiZu8MlM6I3bqGp86uQFKfe2DdFcK4uzCBtd7arIAN1n1vAqFV +8tbyJ7dSmS7ofeo1S2p3/INfWTTzAYxM+lm+BfhiIs8CjbupzCsZ5kGDMVDxf/+i3wQmcG +Z0zsstVBxxNDkAAAEBALxmf4JmEXDPMhcVHLNma11XLjSrOF8lN8oT+XeoMjMNAld2tRlt +/MWhYV23LWniVCuywv6LSt3y5B7/9CC133iMnM7T9EyNxo/KcP6CXMvPXON4uKfqqKLEMb +TnNlB4SdUHjUmlnzZvfXx3njhu4XWCgPfyIPR10Cto3NsW4YpvA6T4eMlC+ewWMEZVus8M +EJlBLECVyFHyVCieZrafjxtMeRylxIHReeIckw5fUFpK67eYcAErb33ERg3lOxw5HBoREx +u8DHKQSIMIKd7RMgmEUhT0UlV+YafoCxgGgW3Hkpd6qLYJ2Egc3CsmiBqB/2/e0sSeNDja +WQzfVgulKR0AAAAnaW5kdXN0cmlhbC1uYXQtbWlndWVmaW5AY3NhTlVDLTIwMjUwOTE5AQ +IDBA== +-----END OPENSSH PRIVATE KEY----- diff --git a/certs/ssh_private_key.pub b/certs/ssh_private_key.pub new file mode 100644 index 0000000..e037274 --- /dev/null +++ b/certs/ssh_private_key.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQClXeuGA1c2NkiZfQMTI4pgL6XEKcD9AZVbXfRnx3umQpM8japD17mYRSzascX0DqOi/QxhTVYmJYe15EjqlxtiBzI8kabgVQgyTPf3NEjej32AxSlUxU2NoKX7sLb2ciUJbUeGov3enk5CYRvRImGy6qgJrmL/nM8Y8spWE8EhzVxZ+poyiJDSjdyVEilp/GwuDN0SurfAp5f8WIX3pgRmOM81CDbqkE8hwDKURKtJzs7hZ0TnWdj622oLTZPnXuw/cIA7SbMElJO3QO1s/ii9hE0IyQ+GPxTQm/G359X3u9Oahr9FgTOyhA4Z9io5r02hStBeSEK297SIccjYzqmtGVl+YwkVc6NtDbD7X2fobgHdA2x6GoOX4WvkYyLt9rGzX9V26+DIqefTksT3aNoKVY3MJcxsX4EyQT3nxXuxQDFjERhvePV0Yx62iB6WGwS+EDVU9OvHnTwJWGsOyna+Jfn5/c2HGzwpxgiiI0LLN/4tH5ICELvNPbIHqEfL0AmTYOb8CxE7v5Gjn138jb4Q0GQhdGGactKic3yIvYzSAjCFA+IC1KSVc0PJ+cuQ+bZaH/l5drLPnreFDPReDdS4bzAoT8Jq01egJRYHPaJ2L8OS8ectorCqjovmUPp6bigBUYy5X1z/2eI7geVQPG4JgGJq2QFJdEk55v9wde0LdQ== industrial-nat-miguefin@csaNUC-20250919 diff --git a/config/nat_config.yaml b/config/nat_config.yaml new file mode 100644 index 0000000..3c7cbe0 --- /dev/null +++ b/config/nat_config.yaml @@ -0,0 +1,50 @@ +# Configuración del sistema NAT industrial +# Para acceso a PLCs/SCADA a través de VPN corporativa + +ssh_server: + host: "91.99.210.72" # PC3 - Servidor Linux intermediario + port: 22 + user: "root" # Usuario SSH verificado + key_file: "/app/certs/ssh_private_key" + +# Reglas NAT predefinidas +nat_rules: + - external_port: 9001 # Puerto expuesto en PC3 + target_ip: "10.1.33.11" # IP del PLC/SCADA + target_port: 5900 # Puerto VNC del PLC + description: "PLC Principal - VNC" + active: true + + - external_port: 9002 + target_ip: "10.1.33.11" + target_port: 80 # Puerto HTTP del PLC + description: "PLC Principal - Web Interface" + active: true + + - external_port: 9003 + target_ip: "10.1.33.12" + target_port: 502 # Modbus TCP + description: "PLC Secundario - Modbus" + active: false + +# Configuración del servidor de gestión +management: + port: 8080 + enabled: true + +# Rango de puertos para asignación automática +auto_port_range: + start: 9000 + end: 9999 + +# Configuración de logging +logging: + level: "INFO" + file: "/app/logs/nat_proxy.log" + max_size_mb: 50 + backup_count: 5 + +# ZeroTier y redes +networks: + corporate_vpn: "10.1.33.0/24" # Red corporativa con PLCs + zerotier: "172.22.0.0/16" # Red ZeroTier (ajustar según tu config) \ No newline at end of file diff --git a/config/proxy_config.yaml b/config/proxy_config.yaml new file mode 100644 index 0000000..80feab7 --- /dev/null +++ b/config/proxy_config.yaml @@ -0,0 +1,29 @@ +# Configuración del servidor SSH (intermediario) +ssh_server: + host: "91.99.210.72" + port: 22 + user: "root" # Cambiar por tu usuario SSH + key_file: "/app/certs/ssh_private_key" + +# Lista de servicios a iniciar automáticamente +services: + - local_port: 3000 + remote_port: 3000 + service_type: "http" + enabled: true + - local_port: 8000 + remote_port: 8000 + service_type: "http" + enabled: true + +# Configuración del servidor de gestión +management: + port: 8080 + enabled: true + +# Configuración de logging +logging: + level: "INFO" + file: "/app/logs/proxy.log" + max_size_mb: 10 + backup_count: 5 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..24afad9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,59 @@ +version: '3.8' + +services: + tcp-proxy: + build: . + container_name: industrial-nat-proxy + restart: always # ⚠️ Reiniciar automáticamente + + # Mapear puertos del host al contenedor + ports: + - "8080:8080" # Puerto de gestión (API REST) + # Los puertos NAT se crean dinámicamente + + # Montar volúmenes para persistencia y configuración + volumes: + - ./config:/app/config:rw # Configuración + - ./certs:/app/certs:ro # Certificados SSH (solo lectura) + - ./logs:/app/logs:rw # Logs + - ./scripts:/app/scripts:rw # Scripts personalizados + + # Variables de entorno + environment: + - PYTHONUNBUFFERED=1 + - CONFIG_FILE=/app/config/nat_config.yaml + - TZ=America/Argentina/Buenos_Aires # Ajustar tu zona horaria + + # Configuración de red + networks: + - proxy-network + + # Configuración de recursos + deploy: + resources: + limits: + memory: 1G + cpus: '1.0' + reservations: + memory: 512M + cpus: '0.5' + + # Configuración de salud + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/status"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + + # Reiniciar en caso de fallo + deploy: + restart_policy: + condition: any + delay: 10s + max_attempts: 5 + window: 120s + +networks: + proxy-network: + driver: bridge \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a5accba --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +asyncio==3.4.3 +aiohttp==3.9.0 +cryptography==41.0.7 +pyyaml==6.0.1 +psutil==5.9.6 +websockets==12.0 +requests==2.31.0 +paramiko==3.3.1 \ No newline at end of file diff --git a/scripts/generate_ssh_key.sh b/scripts/generate_ssh_key.sh new file mode 100755 index 0000000..1fd3993 --- /dev/null +++ b/scripts/generate_ssh_key.sh @@ -0,0 +1,347 @@ +#!/bin/bash + +# Script para generar nueva clave SSH para el sistema NAT industrial +# Ejecutar desde PC1 (WSL2) + +set -e + +echo "🔐 Generación de Clave SSH para Sistema NAT Industrial" +echo "====================================================" + +# Configuración +SSH_KEY_NAME="industrial_nat_key" +SSH_KEY_PATH="/home/miguefin/proxytcp/certs/ssh_private_key" +SSH_PUB_PATH="/home/miguefin/proxytcp/certs/ssh_private_key.pub" +PC3_HOST="91.99.210.72" + +echo "" +echo "📍 Información:" +echo " Clave privada: $SSH_KEY_PATH" +echo " Clave pública: $SSH_PUB_PATH" +echo " Servidor destino: $PC3_HOST" +echo "" + +# Función para generar clave SSH +generate_ssh_key() { + echo "🔑 Generando nueva clave SSH..." + + # Crear directorio si no existe + mkdir -p "$(dirname "$SSH_KEY_PATH")" + + # Generar clave SSH + ssh-keygen -t rsa -b 4096 -f "$SSH_KEY_PATH" -N "" -C "industrial-nat-$(whoami)@$(hostname)-$(date +%Y%m%d)" + + # Establecer permisos correctos + chmod 600 "$SSH_KEY_PATH" + chmod 644 "$SSH_PUB_PATH" + + echo "✅ Clave SSH generada exitosamente" + echo " Privada: $SSH_KEY_PATH" + echo " Pública: $SSH_PUB_PATH" +} + +# Función para mostrar clave pública +show_public_key() { + echo "" + echo "📋 CLAVE PÚBLICA (copiar esta en PC3):" + echo "======================================" + cat "$SSH_PUB_PATH" + echo "======================================" +} + +# Función para crear script de instalación en PC3 +create_pc3_setup_script() { + local setup_script="/tmp/setup_pc3_nat.sh" + + cat > "$setup_script" << 'EOF' +#!/bin/bash + +# Script para configurar PC3 como intermediario SSH NAT +# Ejecutar en PC3 (91.99.210.72) + +set -e + +echo "🖥️ Configurando PC3 como Intermediario SSH NAT" +echo "==============================================" + +# Verificar si somos root o tenemos sudo +if [[ $EUID -eq 0 ]]; then + SUDO="" +else + SUDO="sudo" +fi + +# Función para instalar SSH server si no está +install_ssh_server() { + echo "📦 Verificando SSH Server..." + + if ! systemctl is-active --quiet ssh && ! systemctl is-active --quiet sshd; then + echo " Instalando OpenSSH Server..." + $SUDO apt update + $SUDO apt install -y openssh-server + echo "✅ SSH Server instalado" + else + echo "✅ SSH Server ya está instalado" + fi +} + +# Función para configurar SSH para túneles reversos +configure_ssh() { + echo "⚙️ Configurando SSH para túneles reversos..." + + local ssh_config="/etc/ssh/sshd_config" + local backup_config="/etc/ssh/sshd_config.backup.$(date +%Y%m%d_%H%M%S)" + + # Hacer backup de configuración + $SUDO cp "$ssh_config" "$backup_config" + echo " Backup creado: $backup_config" + + # Configurar GatewayPorts si no está + if ! grep -q "^GatewayPorts yes" "$ssh_config"; then + echo "GatewayPorts yes" | $SUDO tee -a "$ssh_config" > /dev/null + echo " ✅ GatewayPorts habilitado" + else + echo " ✅ GatewayPorts ya estaba habilitado" + fi + + # Configurar AllowTcpForwarding si no está + if ! grep -q "^AllowTcpForwarding yes" "$ssh_config"; then + echo "AllowTcpForwarding yes" | $SUDO tee -a "$ssh_config" > /dev/null + echo " ✅ AllowTcpForwarding habilitado" + else + echo " ✅ AllowTcpForwarding ya estaba habilitado" + fi + + # Reiniciar SSH + echo " Reiniciando SSH Server..." + $SUDO systemctl restart ssh 2>/dev/null || $SUDO systemctl restart sshd + echo "✅ SSH Server reiniciado" +} + +# Función para configurar firewall +configure_firewall() { + echo "🔥 Configurando Firewall..." + + # Verificar si ufw está instalado y activo + if command -v ufw >/dev/null 2>&1; then + if $SUDO ufw status | grep -q "Status: active"; then + echo " UFW está activo, configurando reglas..." + $SUDO ufw allow 22/tcp comment "SSH" + $SUDO ufw allow 9000:9999/tcp comment "NAT Tunnels" + echo "✅ Reglas UFW configuradas" + else + echo " UFW está instalado pero inactivo" + fi + else + echo " UFW no está instalado" + fi + + # Verificar iptables básico + if command -v iptables >/dev/null 2>&1; then + echo " Verificando reglas iptables básicas..." + # Solo mostrar info, no modificar iptables automáticamente + echo " ℹ️ Si usas iptables, asegúrate de permitir puertos 22 y 9000-9999" + fi +} + +# Función para configurar clave SSH +setup_ssh_key() { + echo "🔑 Configurando autenticación por clave SSH..." + + read -p "👤 ¿Cuál es tu usuario SSH en este servidor? (default: $USER): " ssh_user + ssh_user=${ssh_user:-$USER} + + local home_dir + if [[ "$ssh_user" == "root" ]]; then + home_dir="/root" + else + home_dir="/home/$ssh_user" + fi + + local ssh_dir="$home_dir/.ssh" + local auth_keys="$ssh_dir/authorized_keys" + + echo " Configurando para usuario: $ssh_user" + echo " Directorio SSH: $ssh_dir" + + # Crear directorio .ssh si no existe + if [[ "$ssh_user" == "$USER" ]]; then + mkdir -p "$ssh_dir" + chmod 700 "$ssh_dir" + else + $SUDO mkdir -p "$ssh_dir" + $SUDO chmod 700 "$ssh_dir" + $SUDO chown "$ssh_user:$ssh_user" "$ssh_dir" + fi + + echo "" + echo "📋 AHORA NECESITAS AGREGAR TU CLAVE PÚBLICA" + echo "===========================================" + echo "1. Copia la clave pública que se mostró en PC1" + echo "2. Pégala cuando se solicite" + echo "" + echo "Formato esperado: ssh-rsa AAAAB3NzaC1yc2EAAA... comentario" + echo "" + + read -p "📝 Pega tu clave pública SSH aquí: " public_key + + if [[ -z "$public_key" ]]; then + echo "❌ No se proporcionó clave pública" + return 1 + fi + + # Validar formato básico de clave SSH + if [[ "$public_key" =~ ^(ssh-rsa|ssh-ed25519|ecdsa-sha2-) ]]; then + # Agregar clave a authorized_keys + if [[ "$ssh_user" == "$USER" ]]; then + echo "$public_key" >> "$auth_keys" + chmod 600 "$auth_keys" + else + echo "$public_key" | $SUDO tee -a "$auth_keys" > /dev/null + $SUDO chmod 600 "$auth_keys" + $SUDO chown "$ssh_user:$ssh_user" "$auth_keys" + fi + + echo "✅ Clave SSH agregada para usuario $ssh_user" + else + echo "❌ Formato de clave SSH inválido" + return 1 + fi +} + +# Función para verificar configuración +verify_setup() { + echo "🧪 Verificando configuración..." + + # Verificar SSH está ejecutándose + if systemctl is-active --quiet ssh || systemctl is-active --quiet sshd; then + echo "✅ SSH Server está ejecutándose" + else + echo "❌ SSH Server no está ejecutándose" + return 1 + fi + + # Verificar puerto 22 + if netstat -tlnp 2>/dev/null | grep -q ":22 " || ss -tlnp 2>/dev/null | grep -q ":22 "; then + echo "✅ Puerto 22 está abierto" + else + echo "❌ Puerto 22 no está escuchando" + return 1 + fi + + # Mostrar información de conexión + local external_ip + external_ip=$(curl -s ifconfig.me 2>/dev/null || curl -s ipinfo.io/ip 2>/dev/null || echo "IP_NO_DETECTADA") + + echo "" + echo "📊 INFORMACIÓN DE CONEXIÓN:" + echo "==========================" + echo " IP Externa: $external_ip" + echo " Puerto SSH: 22" + echo " Rango puertos NAT: 9000-9999" + echo "" +} + +# Función principal +main() { + echo "Iniciando configuración de PC3..." + echo "" + + install_ssh_server + configure_ssh + configure_firewall + setup_ssh_key + verify_setup + + echo "" + echo "🎉 ¡Configuración de PC3 completada!" + echo "" + echo "📋 PRÓXIMOS PASOS:" + echo "1. Probar conexión SSH desde PC1:" + echo " ssh -i certs/ssh_private_key usuario@$(hostname -I | awk '{print $1}')" + echo "" + echo "2. Si la conexión SSH funciona, ejecutar en PC1:" + echo " ./setup.sh" + echo "" + echo "3. Usar el sistema NAT desde PC2:" + echo " python nat_client.py plc 10.1.33.11 vnc" + echo "" +} + +# Ejecutar función principal +main "$@" +EOF + + chmod +x "$setup_script" + echo "" + echo "📄 Script de configuración de PC3 creado: $setup_script" +} + +# Función principal +main() { + echo "Generando nueva clave SSH para el sistema NAT industrial..." + echo "" + + # Verificar si ya existe una clave + if [[ -f "$SSH_KEY_PATH" ]]; then + echo "⚠️ Ya existe una clave SSH en: $SSH_KEY_PATH" + echo "" + read -p "¿Quieres sobrescribirla? (y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "❌ Operación cancelada" + exit 1 + fi + echo "" + fi + + # Generar nueva clave SSH + generate_ssh_key + + # Mostrar clave pública + show_public_key + + # Crear script para PC3 + create_pc3_setup_script + + echo "" + echo "🎯 PRÓXIMOS PASOS:" + echo "=================" + echo "" + echo "1. 📋 COPIAR la clave pública mostrada arriba" + echo "" + echo "2. 🖥️ En PC3 (91.99.210.72), ejecutar:" + echo " wget -O- https://pastebin.com/raw/XXXXXXXX | bash" + echo " (o copiar manualmente el script /tmp/setup_pc3_nat.sh)" + echo "" + echo "3. 🧪 PROBAR conexión SSH desde aquí:" + echo " ssh -i $SSH_KEY_PATH usuario@$PC3_HOST" + echo "" + echo "4. 🚀 Si funciona, ejecutar:" + echo " ./setup.sh" + echo "" + echo "5. 📱 Usar desde PC2:" + echo " python nat_client.py plc 10.1.33.11 vnc" + echo "" + + # Mostrar información adicional + echo "ℹ️ INFORMACIÓN DE LA CLAVE:" + echo " Tipo: RSA 4096 bits" + echo " Privada: $SSH_KEY_PATH" + echo " Pública: $SSH_PUB_PATH" + echo " Propósito: Sistema NAT Industrial" + echo "" + + # Verificar si ssh-agent está disponible para cargar la clave + if command -v ssh-agent >/dev/null 2>&1; then + echo "💡 CONSEJO: Para cargar la clave en ssh-agent:" + echo " eval \$(ssh-agent -s)" + echo " ssh-add $SSH_KEY_PATH" + echo "" + fi + + echo "✅ ¡Nueva clave SSH generada exitosamente!" +} + +# Ejecutar función principal +main "$@" \ No newline at end of file diff --git a/scripts/industrial_manager.sh b/scripts/industrial_manager.sh new file mode 100755 index 0000000..f0db537 --- /dev/null +++ b/scripts/industrial_manager.sh @@ -0,0 +1,283 @@ +#!/bin/bash + +# Script de ejemplo para gestión industrial automatizada +# Conecta automáticamente a PLCs comunes de una línea de producción + +set -e + +echo "🏭 Configuración Industrial NAT para PLCs/SCADA" +echo "================================================" + +# Configuración de PLCs comunes +declare -A PLCS=( + ["10.1.33.11"]="PLC Principal - Línea 1" + ["10.1.33.12"]="PLC Secundario - Línea 1" + ["10.1.33.13"]="HMI Operador" + ["10.1.33.20"]="Historiador SCADA" + ["10.1.33.30"]="Switch Industrial" +) + +# Servicios por defecto +declare -A SERVICES=( + ["vnc"]="5900" + ["web"]="80" + ["modbus"]="502" + ["ssh"]="22" + ["https"]="443" +) + +# Función para mostrar menú +show_menu() { + echo "" + echo "Opciones disponibles:" + echo "1. Conectar a PLC específico" + echo "2. Conectar a todos los PLCs (VNC)" + echo "3. Configurar acceso web a PLCs" + echo "4. Acceso Modbus a controladores" + echo "5. Ver estado del sistema" + echo "6. Limpiar todas las conexiones" + echo "7. Configuración personalizada" + echo "8. Salir" + echo "" +} + +# Función para conectar a PLC específico +connect_specific_plc() { + echo "" + echo "PLCs disponibles:" + local i=1 + local plc_ips=() + + for ip in "${!PLCS[@]}"; do + echo "$i. $ip - ${PLCS[$ip]}" + plc_ips+=("$ip") + ((i++)) + done + + echo "" + read -p "Selecciona PLC (1-${#PLCS[@]}): " plc_choice + + if [[ $plc_choice -ge 1 && $plc_choice -le ${#PLCS[@]} ]]; then + local selected_ip="${plc_ips[$((plc_choice-1))]}" + + echo "" + echo "Servicios disponibles:" + echo "1. VNC (5900) - Acceso gráfico" + echo "2. Web (80) - Interface web" + echo "3. HTTPS (443) - Interface web segura" + echo "4. Modbus (502) - Comunicación industrial" + echo "5. SSH (22) - Terminal" + + read -p "Selecciona servicio (1-5): " service_choice + + case $service_choice in + 1) service="vnc" ;; + 2) service="web" ;; + 3) service="https" ;; + 4) service="modbus" ;; + 5) service="ssh" ;; + *) echo "Opción inválida"; return ;; + esac + + echo "" + echo "Conectando a ${PLCS[$selected_ip]} ($selected_ip) - $service..." + + if python3 /home/miguefin/proxytcp/scripts/nat_client.py plc "$selected_ip" "$service" --wait; then + echo "" + echo "✅ ¡Conexión establecida exitosamente!" + echo "📱 Desde PC2 conecta a: 91.99.210.72:(puerto asignado)" + + if [[ "$service" == "vnc" ]]; then + echo "🖥️ Usar VNC Viewer con la dirección mostrada arriba" + elif [[ "$service" == "web" || "$service" == "https" ]]; then + echo "🌐 Abrir navegador web con la dirección mostrada arriba" + fi + else + echo "❌ Error estableciendo conexión" + fi + else + echo "Selección inválida" + fi +} + +# Función para conectar todos los PLCs +connect_all_plcs() { + echo "" + echo "🔗 Conectando a todos los PLCs con VNC..." + + for ip in "${!PLCS[@]}"; do + echo "" + echo "Conectando a ${PLCS[$ip]} ($ip)..." + + if python3 /home/miguefin/proxytcp/scripts/nat_client.py plc "$ip" vnc; then + echo "✅ $ip conectado" + else + echo "❌ Error conectando a $ip" + fi + + sleep 1 + done + + echo "" + echo "📋 Resumen de conexiones:" + python3 /home/miguefin/proxytcp/scripts/nat_client.py list +} + +# Función para acceso web +setup_web_access() { + echo "" + echo "🌐 Configurando acceso web a PLCs..." + + for ip in "${!PLCS[@]}"; do + echo "Configurando web para ${PLCS[$ip]} ($ip)..." + + if python3 /home/miguefin/proxytcp/scripts/nat_client.py plc "$ip" web; then + echo "✅ Web access configurado para $ip" + else + echo "⚠️ No se pudo configurar web para $ip" + fi + done + + echo "" + echo "🌐 Acceso web configurado. Desde PC2 usar navegador con:" + python3 /home/miguefin/proxytcp/scripts/nat_client.py list | grep -i web +} + +# Función para acceso Modbus +setup_modbus_access() { + echo "" + echo "🔧 Configurando acceso Modbus TCP..." + + # Solo PLCs que normalmente tienen Modbus + modbus_plcs=("10.1.33.11" "10.1.33.12") + + for ip in "${modbus_plcs[@]}"; do + if [[ -n "${PLCS[$ip]}" ]]; then + echo "Configurando Modbus para ${PLCS[$ip]} ($ip)..." + + if python3 /home/miguefin/proxytcp/scripts/nat_client.py plc "$ip" modbus; then + echo "✅ Modbus TCP configurado para $ip" + else + echo "⚠️ No se pudo configurar Modbus para $ip" + fi + fi + done + + echo "" + echo "🔧 Acceso Modbus configurado. Configurar cliente Modbus con:" + python3 /home/miguefin/proxytcp/scripts/nat_client.py list | grep -i modbus +} + +# Función para mostrar estado +show_status() { + echo "" + echo "📊 Estado del Sistema NAT Industrial:" + echo "====================================" + + if python3 /home/miguefin/proxytcp/scripts/nat_client.py status; then + echo "" + echo "📋 Conexiones Activas:" + python3 /home/miguefin/proxytcp/scripts/nat_client.py list + else + echo "❌ No se pudo obtener el estado del sistema" + echo " Verifica que el contenedor esté ejecutándose" + fi +} + +# Función para limpiar conexiones +cleanup_connections() { + echo "" + echo "🧹 Limpiando todas las conexiones..." + + # Obtener lista de puertos activos y eliminarlos + if command -v jq &> /dev/null; then + # Si jq está disponible, usar parsing JSON + ports=$(python3 /home/miguefin/proxytcp/scripts/nat_client.py status 2>/dev/null | jq -r '.rules[].external_port' 2>/dev/null) + else + # Método alternativo sin jq + ports=$(python3 /home/miguefin/proxytcp/scripts/nat_client.py list 2>/dev/null | grep -oE '91\.99\.210\.72:([0-9]+)' | cut -d: -f2) + fi + + if [[ -n "$ports" ]]; then + for port in $ports; do + echo "Eliminando conexión en puerto $port..." + python3 /home/miguefin/proxytcp/scripts/nat_client.py remove "$port" >/dev/null 2>&1 || true + done + echo "✅ Conexiones eliminadas" + else + echo "ℹ️ No hay conexiones activas para eliminar" + fi +} + +# Función para configuración personalizada +custom_config() { + echo "" + echo "⚙️ Configuración Personalizada" + echo "=============================" + + read -p "IP del dispositivo: " custom_ip + read -p "Puerto del dispositivo: " custom_port + read -p "Descripción (opcional): " custom_desc + + echo "" + echo "Creando conexión personalizada..." + + if python3 /home/miguefin/proxytcp/scripts/nat_client.py connect "$custom_ip" "$custom_port" --description "$custom_desc" --wait; then + echo "✅ Conexión personalizada establecida" + else + echo "❌ Error creando conexión personalizada" + fi +} + +# Verificar dependencias +check_dependencies() { + if ! command -v python3 &> /dev/null; then + echo "❌ Python3 no está instalado" + exit 1 + fi + + if ! python3 -c "import requests" &> /dev/null; then + echo "⚠️ Módulo 'requests' no está disponible" + echo " Instalando: pip3 install requests" + pip3 install requests >/dev/null 2>&1 || { + echo "❌ No se pudo instalar 'requests'" + exit 1 + } + fi +} + +# Función principal +main() { + echo "🏭 Sistema NAT Industrial - Gestión de PLCs/SCADA" + echo "==================================================" + + check_dependencies + + while true; do + show_menu + read -p "Selecciona una opción (1-8): " choice + + case $choice in + 1) connect_specific_plc ;; + 2) connect_all_plcs ;; + 3) setup_web_access ;; + 4) setup_modbus_access ;; + 5) show_status ;; + 6) cleanup_connections ;; + 7) custom_config ;; + 8) + echo "👋 ¡Hasta luego!" + exit 0 + ;; + *) + echo "❌ Opción inválida. Selecciona 1-8." + ;; + esac + + echo "" + read -p "Presiona Enter para continuar..." + done +} + +# Ejecutar función principal +main "$@" \ No newline at end of file diff --git a/scripts/manage_proxy.sh b/scripts/manage_proxy.sh new file mode 100755 index 0000000..5a4c6c8 --- /dev/null +++ b/scripts/manage_proxy.sh @@ -0,0 +1,172 @@ +#!/bin/bash + +# Script para gestionar el proxy TCP de forma dinámica +# Uso: ./manage_proxy.sh [comando] [argumentos] + +PROXY_URL="http://localhost:8080" + +show_help() { + echo "Uso: $0 [comando] [argumentos]" + echo "" + echo "Comandos disponibles:" + echo " status - Mostrar estado de todos los proxies" + echo " add - Añadir nuevo proxy" + echo " remove - Eliminar proxy existente" + echo " logs - Mostrar logs del contenedor" + echo " restart - Reiniciar el contenedor" + echo " build - Construir la imagen del contenedor" + echo " start - Iniciar el contenedor" + echo " stop - Detener el contenedor" + echo "" + echo "Ejemplos:" + echo " $0 status" + echo " $0 add 9000 9000" + echo " $0 remove 9000" +} + +check_container() { + if ! docker ps | grep -q "tcp-proxy-container"; then + echo "Error: El contenedor tcp-proxy no está ejecutándose" + echo "Ejecuta: $0 start" + exit 1 + fi +} + +get_status() { + check_container + echo "Estado de los proxies TCP:" + curl -s "$PROXY_URL/status" | python3 -m json.tool 2>/dev/null || echo "Error obteniendo estado" +} + +add_proxy() { + local local_port=$1 + local remote_port=$2 + + if [[ -z "$local_port" || -z "$remote_port" ]]; then + echo "Error: Debes especificar puerto local y remoto" + echo "Uso: $0 add " + exit 1 + fi + + check_container + + echo "Añadiendo proxy: localhost:$local_port -> 91.99.210.72:$remote_port" + + response=$(curl -s -X POST "$PROXY_URL/add" \ + -H "Content-Type: application/json" \ + -d "{\"local_port\": $local_port, \"remote_port\": $remote_port}") + + echo "$response" | python3 -m json.tool 2>/dev/null || echo "Respuesta: $response" + + # Verificar si necesitamos exponer el puerto en docker-compose + if ! grep -q "$local_port:$local_port" docker-compose.yml; then + echo "" + echo "⚠️ IMPORTANTE: Agrega la siguiente línea en docker-compose.yml bajo 'ports':" + echo " - \"$local_port:$local_port\"" + echo " Luego ejecuta: $0 restart" + fi +} + +remove_proxy() { + local local_port=$1 + + if [[ -z "$local_port" ]]; then + echo "Error: Debes especificar el puerto local" + echo "Uso: $0 remove " + exit 1 + fi + + check_container + + echo "Eliminando proxy del puerto $local_port" + + response=$(curl -s -X POST "$PROXY_URL/remove" \ + -H "Content-Type: application/json" \ + -d "{\"local_port\": $local_port}") + + echo "$response" | python3 -m json.tool 2>/dev/null || echo "Respuesta: $response" +} + +show_logs() { + echo "Logs del contenedor tcp-proxy:" + docker logs -f tcp-proxy-container +} + +restart_container() { + echo "Reiniciando contenedor..." + docker-compose down + docker-compose up -d + echo "Contenedor reiniciado" +} + +build_container() { + echo "Construyendo imagen del contenedor..." + docker-compose build + echo "Imagen construida" +} + +start_container() { + echo "Iniciando contenedor..." + docker-compose up -d + echo "Contenedor iniciado" + + # Esperar a que el servicio esté listo + echo "Esperando a que el servicio esté listo..." + for i in {1..30}; do + if curl -s "$PROXY_URL/status" >/dev/null 2>&1; then + echo "✅ Servicio listo" + break + fi + sleep 1 + echo -n "." + done + echo "" +} + +stop_container() { + echo "Deteniendo contenedor..." + docker-compose down + echo "Contenedor detenido" +} + +# Verificar argumentos +if [[ $# -eq 0 ]]; then + show_help + exit 1 +fi + +# Procesar comandos +case "$1" in + "status") + get_status + ;; + "add") + add_proxy "$2" "$3" + ;; + "remove") + remove_proxy "$2" + ;; + "logs") + show_logs + ;; + "restart") + restart_container + ;; + "build") + build_container + ;; + "start") + start_container + ;; + "stop") + stop_container + ;; + "help"|"-h"|"--help") + show_help + ;; + *) + echo "Comando desconocido: $1" + show_help + exit 1 + ;; +esac \ No newline at end of file diff --git a/scripts/nat_client.py b/scripts/nat_client.py new file mode 100755 index 0000000..94ce88c --- /dev/null +++ b/scripts/nat_client.py @@ -0,0 +1,277 @@ +#!/usr/bin/env python3 +""" +Cliente para gestión del sistema NAT industrial +Permite a PC2 crear conexiones dinámicas a PLCs/SCADA +""" + +import requests +import json +import sys +import argparse +import time +from typing import Dict, List, Optional + +class IndustrialNATClient: + def __init__(self, manager_url: str = "http://91.99.210.72:8080"): + """ + Cliente para gestionar NAT industrial + + Args: + manager_url: URL del gestor NAT (por defecto PC3:8080) + """ + self.manager_url = manager_url.rstrip('/') + + def get_status(self) -> Dict: + """Obtiene estado completo del sistema NAT""" + try: + response = requests.get(f"{self.manager_url}/status", timeout=10) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + raise Exception(f"Error obteniendo estado: {e}") + + def quick_connect(self, target_ip: str, target_port: int, description: str = "") -> Dict: + """ + Conexión rápida - asigna puerto automáticamente + + Args: + target_ip: IP del PLC/dispositivo (ej: 10.1.33.11) + target_port: Puerto del dispositivo (ej: 5900 para VNC) + description: Descripción opcional + + Returns: + Dict con información de conexión + """ + try: + data = { + "target_ip": target_ip, + "target_port": target_port, + "description": description or f"Quick connect to {target_ip}:{target_port}" + } + + response = requests.post( + f"{self.manager_url}/quick-connect", + json=data, + timeout=30 # Puede tardar en establecer túnel SSH + ) + response.raise_for_status() + return response.json() + + except requests.exceptions.RequestException as e: + raise Exception(f"Error creando conexión: {e}") + + def add_nat_rule(self, target_ip: str, target_port: int, + external_port: Optional[int] = None, description: str = "") -> Dict: + """ + Añade regla NAT específica + + Args: + target_ip: IP del PLC/dispositivo + target_port: Puerto del dispositivo + external_port: Puerto específico en PC3 (opcional) + description: Descripción + """ + try: + data = { + "target_ip": target_ip, + "target_port": target_port, + "description": description + } + + if external_port: + data["external_port"] = external_port + + response = requests.post( + f"{self.manager_url}/add", + json=data, + timeout=30 + ) + response.raise_for_status() + return response.json() + + except requests.exceptions.RequestException as e: + raise Exception(f"Error añadiendo regla NAT: {e}") + + def remove_nat_rule(self, external_port: int) -> Dict: + """Elimina regla NAT""" + try: + data = {"external_port": external_port} + response = requests.post( + f"{self.manager_url}/remove", + json=data, + timeout=10 + ) + response.raise_for_status() + return response.json() + + except requests.exceptions.RequestException as e: + raise Exception(f"Error eliminando regla NAT: {e}") + + def list_active_connections(self) -> List[Dict]: + """Lista todas las conexiones activas""" + status = self.get_status() + return [ + rule for rule in status['rules'] + if rule['proxy_running'] and rule['tunnel_running'] + ] + + def connect_to_plc(self, plc_ip: str, service: str = "vnc") -> Dict: + """ + Método de conveniencia para conectar a PLCs comunes + + Args: + plc_ip: IP del PLC + service: Tipo de servicio ('vnc', 'web', 'modbus', 'ssh', 'telnet') + """ + service_ports = { + 'vnc': 5900, + 'web': 80, + 'https': 443, + 'modbus': 502, + 'ssh': 22, + 'telnet': 23, + 'ftp': 21, + 'http-alt': 8080 + } + + if service not in service_ports: + raise ValueError(f"Servicio desconocido: {service}. Disponibles: {list(service_ports.keys())}") + + port = service_ports[service] + description = f"PLC {plc_ip} - {service.upper()}" + + return self.quick_connect(plc_ip, port, description) + + def wait_for_connection(self, external_port: int, timeout: int = 60) -> bool: + """Espera a que la conexión esté lista""" + start_time = time.time() + + while time.time() - start_time < timeout: + try: + status = self.get_status() + for rule in status['rules']: + if (rule['external_port'] == external_port and + rule['proxy_running'] and rule['tunnel_running']): + return True + + except Exception: + pass + + time.sleep(2) + + return False + +def main(): + parser = argparse.ArgumentParser(description="Cliente NAT Industrial para acceso a PLCs/SCADA") + parser.add_argument("--url", default="http://91.99.210.72:8080", + help="URL del gestor NAT (default: PC3)") + + subparsers = parser.add_subparsers(dest="command", help="Comandos disponibles") + + # Comando status + subparsers.add_parser("status", help="Mostrar estado del sistema NAT") + + # Comando quick-connect + quick_parser = subparsers.add_parser("connect", help="Conexión rápida a PLC/dispositivo") + quick_parser.add_argument("target_ip", help="IP del PLC/dispositivo (ej: 10.1.33.11)") + quick_parser.add_argument("target_port", type=int, help="Puerto del dispositivo") + quick_parser.add_argument("--description", default="", help="Descripción opcional") + quick_parser.add_argument("--wait", action="store_true", help="Esperar a que la conexión esté lista") + + # Comando plc (conveniencia) + plc_parser = subparsers.add_parser("plc", help="Conectar a PLC con servicios predefinidos") + plc_parser.add_argument("plc_ip", help="IP del PLC") + plc_parser.add_argument("service", choices=['vnc', 'web', 'https', 'modbus', 'ssh', 'telnet', 'ftp', 'http-alt'], + help="Tipo de servicio") + plc_parser.add_argument("--wait", action="store_true", help="Esperar a que la conexión esté lista") + + # Comando add + add_parser = subparsers.add_parser("add", help="Añadir regla NAT específica") + add_parser.add_argument("target_ip", help="IP del dispositivo") + add_parser.add_argument("target_port", type=int, help="Puerto del dispositivo") + add_parser.add_argument("--external-port", type=int, help="Puerto específico en PC3") + add_parser.add_argument("--description", default="", help="Descripción") + + # Comando remove + remove_parser = subparsers.add_parser("remove", help="Eliminar regla NAT") + remove_parser.add_argument("external_port", type=int, help="Puerto externo a eliminar") + + # Comando list + subparsers.add_parser("list", help="Listar conexiones activas") + + args = parser.parse_args() + + if not args.command: + parser.print_help() + return 1 + + client = IndustrialNATClient(args.url) + + try: + if args.command == "status": + status = client.get_status() + print(json.dumps(status, indent=2)) + + elif args.command == "connect": + print(f"Conectando a {args.target_ip}:{args.target_port}...") + result = client.quick_connect(args.target_ip, args.target_port, args.description) + + if result['success']: + print(f"✅ Conexión establecida!") + print(f" Acceso desde PC2: {result['access_url']}") + print(f" Puerto asignado: {result['external_port']}") + + if args.wait: + print("⏳ Esperando que la conexión esté lista...") + if client.wait_for_connection(result['external_port']): + print("✅ Conexión lista!") + else: + print("⚠️ Timeout esperando conexión") + else: + print(f"❌ Error: {result['error']}") + return 1 + + elif args.command == "plc": + print(f"Conectando a PLC {args.plc_ip} ({args.service})...") + result = client.connect_to_plc(args.plc_ip, args.service) + + if result['success']: + print(f"✅ Conexión a PLC establecida!") + print(f" Acceso desde PC2: {result['access_url']}") + print(f" Servicio: {args.service.upper()}") + + if args.wait: + print("⏳ Esperando que la conexión esté lista...") + if client.wait_for_connection(result['external_port']): + print("✅ PLC accesible!") + else: + print("⚠️ Timeout esperando conexión") + else: + print(f"❌ Error: {result['error']}") + return 1 + + elif args.command == "add": + result = client.add_nat_rule( + args.target_ip, args.target_port, + args.external_port, args.description + ) + print(json.dumps(result, indent=2)) + + elif args.command == "remove": + result = client.remove_nat_rule(args.external_port) + print(json.dumps(result, indent=2)) + + elif args.command == "list": + connections = client.list_active_connections() + print(f"Conexiones activas: {len(connections)}") + for conn in connections: + print(f" {conn['access_url']} -> {conn['target_ip']}:{conn['target_port']} ({conn['description']})") + + return 0 + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/scripts/proxy_client.py b/scripts/proxy_client.py new file mode 100755 index 0000000..cd8d9fc --- /dev/null +++ b/scripts/proxy_client.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +""" +Script de gestión de proxies TCP - Versión Python +Permite gestionar los proxies desde Python para automatización +""" + +import requests +import json +import sys +import argparse +import subprocess +import time + +class ProxyManager: + def __init__(self, base_url="http://localhost:8080"): + self.base_url = base_url + + def check_container(self): + """Verifica que el contenedor esté ejecutándose""" + try: + result = subprocess.run( + ["docker", "ps", "--filter", "name=tcp-proxy-container", "--format", "{{.Names}}"], + capture_output=True, text=True, check=True + ) + if "tcp-proxy-container" not in result.stdout: + raise Exception("Contenedor tcp-proxy no está ejecutándose") + except subprocess.CalledProcessError: + raise Exception("Error verificando estado del contenedor") + + def get_status(self): + """Obtiene el estado de todos los proxies""" + try: + self.check_container() + response = requests.get(f"{self.base_url}/status", timeout=10) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + raise Exception(f"Error obteniendo estado: {e}") + + def add_proxy(self, local_port, remote_port): + """Añade un nuevo proxy""" + try: + self.check_container() + data = { + "local_port": int(local_port), + "remote_port": int(remote_port) + } + response = requests.post( + f"{self.base_url}/add", + json=data, + timeout=10 + ) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + raise Exception(f"Error añadiendo proxy: {e}") + + def remove_proxy(self, local_port): + """Elimina un proxy existente""" + try: + self.check_container() + data = {"local_port": int(local_port)} + response = requests.post( + f"{self.base_url}/remove", + json=data, + timeout=10 + ) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + raise Exception(f"Error eliminando proxy: {e}") + + def wait_for_service(self, timeout=30): + """Espera a que el servicio esté disponible""" + start_time = time.time() + while time.time() - start_time < timeout: + try: + requests.get(f"{self.base_url}/status", timeout=5) + return True + except: + time.sleep(1) + return False + +def main(): + parser = argparse.ArgumentParser(description="Gestión de proxies TCP") + parser.add_argument("--url", default="http://localhost:8080", + help="URL base del servicio (default: http://localhost:8080)") + + subparsers = parser.add_subparsers(dest="command", help="Comandos disponibles") + + # Comando status + subparsers.add_parser("status", help="Mostrar estado de todos los proxies") + + # Comando add + add_parser = subparsers.add_parser("add", help="Añadir nuevo proxy") + add_parser.add_argument("local_port", type=int, help="Puerto local") + add_parser.add_argument("remote_port", type=int, help="Puerto remoto") + + # Comando remove + remove_parser = subparsers.add_parser("remove", help="Eliminar proxy") + remove_parser.add_argument("local_port", type=int, help="Puerto local a eliminar") + + # Comando wait + wait_parser = subparsers.add_parser("wait", help="Esperar a que el servicio esté listo") + wait_parser.add_argument("--timeout", type=int, default=30, help="Timeout en segundos") + + args = parser.parse_args() + + if not args.command: + parser.print_help() + return 1 + + manager = ProxyManager(args.url) + + try: + if args.command == "status": + status = manager.get_status() + print(json.dumps(status, indent=2)) + + elif args.command == "add": + result = manager.add_proxy(args.local_port, args.remote_port) + print(json.dumps(result, indent=2)) + + elif args.command == "remove": + result = manager.remove_proxy(args.local_port) + print(json.dumps(result, indent=2)) + + elif args.command == "wait": + print(f"Esperando a que el servicio esté disponible (timeout: {args.timeout}s)...") + if manager.wait_for_service(args.timeout): + print("✅ Servicio disponible") + return 0 + else: + print("❌ Timeout esperando al servicio") + return 1 + + return 0 + + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..b4f6534 --- /dev/null +++ b/setup.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +# Script de ejemplo para automatizar la gestión de proxies +# Úsalo como base para tus propios scripts de automatización + +set -e + +echo "🚀 Iniciando configuración automática del proxy TCP..." + +# 1. Verificar que Docker está disponible +if ! command -v docker &> /dev/null; then + echo "❌ Docker no está instalado" + exit 1 +fi + +if ! command -v docker-compose &> /dev/null; then + echo "❌ Docker Compose no está instalado" + exit 1 +fi + +echo "✅ Docker disponible" + +# 2. Verificar certificados +if [[ ! -f "certs/client.crt" || ! -f "certs/client.key" || ! -f "certs/ca.crt" ]]; then + echo "⚠️ Advertencia: Faltan certificados SSL" + echo " Coloca tus certificados en:" + echo " - certs/client.crt" + echo " - certs/client.key" + echo " - certs/ca.crt" + echo "" + read -p "¿Continuar sin certificados? (y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi +fi + +# 3. Construir imagen +echo "🔨 Construyendo imagen Docker..." +./scripts/manage_proxy.sh build + +# 4. Iniciar contenedor +echo "🚀 Iniciando contenedor..." +./scripts/manage_proxy.sh start + +# 5. Esperar a que esté listo +echo "⏳ Esperando a que el servicio esté listo..." +./scripts/proxy_client.py wait --timeout 60 + +# 6. Configurar proxies predeterminados +echo "⚙️ Configurando proxies predeterminados..." + +# Ejemplo: aplicación web +./scripts/manage_proxy.sh add 3000 3000 +echo "✅ Proxy web añadido: localhost:3000" + +# Ejemplo: API +./scripts/manage_proxy.sh add 8000 8000 +echo "✅ Proxy API añadido: localhost:8000" + +# 7. Mostrar estado final +echo "" +echo "🎉 ¡Configuración completada!" +echo "" +./scripts/manage_proxy.sh status +echo "" +echo "📖 Comandos útiles:" +echo " Ver estado: ./scripts/manage_proxy.sh status" +echo " Añadir proxy: ./scripts/manage_proxy.sh add [puerto_local] [puerto_remoto]" +echo " Ver logs: ./scripts/manage_proxy.sh logs" +echo " Ayuda: ./scripts/manage_proxy.sh help" \ No newline at end of file diff --git a/setup_permanent.sh b/setup_permanent.sh new file mode 100755 index 0000000..3911c7b --- /dev/null +++ b/setup_permanent.sh @@ -0,0 +1,311 @@ +#!/bin/bash + +# Script para configurar el sistema NAT industrial como servicio permanente +# Ejecutar desde PC1 (WSL2) + +set -e + +echo "🏭 Configuración de Sistema NAT Industrial Permanente" +echo "====================================================" + +# Función para verificar Docker +check_docker() { + echo "🐳 Verificando Docker..." + + if ! command -v docker &> /dev/null; then + echo "❌ Docker no está instalado" + echo " Instalar con: curl -fsSL https://get.docker.com | sh" + exit 1 + fi + + if ! docker info &> /dev/null; then + echo "⚠️ Docker no está ejecutándose, iniciando..." + sudo service docker start || { + echo "❌ No se pudo iniciar Docker" + exit 1 + } + fi + + if ! command -v docker-compose &> /dev/null; then + echo "⚠️ Docker Compose no está instalado, instalando..." + sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + fi + + echo "✅ Docker está listo" +} + +# Función para verificar conexión SSH +test_ssh_connection() { + echo "🔐 Verificando conexión SSH a PC3..." + + local ssh_host=$(grep "host:" config/nat_config.yaml | awk '{print $2}' | tr -d '"') + local ssh_user=$(grep "user:" config/nat_config.yaml | awk '{print $2}' | tr -d '"') + local ssh_key="certs/ssh_private_key" + + echo " Host: $ssh_host" + echo " Usuario: $ssh_user" + echo " Clave: $ssh_key" + + if ssh -i "$ssh_key" -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$ssh_user@$ssh_host" "echo 'SSH OK'" &>/dev/null; then + echo "✅ Conexión SSH exitosa" + else + echo "❌ Error de conexión SSH" + echo " Verifica:" + echo " 1. PC3 ($ssh_host) está accesible" + echo " 2. Usuario '$ssh_user' es correcto" + echo " 3. Clave SSH está configurada en PC3" + return 1 + fi +} + +# Función para crear directorios necesarios +create_directories() { + echo "📁 Creando directorios necesarios..." + + mkdir -p logs + mkdir -p config + mkdir -p certs + + # Establecer permisos correctos + chmod 700 certs + chmod 600 certs/ssh_private_key 2>/dev/null || true + + echo "✅ Directorios creados" +} + +# Función para construir imagen Docker +build_image() { + echo "🔨 Construyendo imagen Docker..." + + if docker-compose build; then + echo "✅ Imagen construida exitosamente" + else + echo "❌ Error construyendo imagen" + return 1 + fi +} + +# Función para configurar como servicio del sistema +create_systemd_service() { + local service_name="industrial-nat" + local service_file="/etc/systemd/system/${service_name}.service" + local work_dir="$(pwd)" + + echo "⚙️ Configurando servicio systemd..." + + # Crear archivo de servicio systemd + sudo tee "$service_file" > /dev/null << EOF +[Unit] +Description=Industrial NAT Proxy Service +Documentation=https://github.com/user/industrial-nat +After=docker.service +Requires=docker.service + +[Service] +Type=oneshot +RemainAfterExit=yes +WorkingDirectory=$work_dir +ExecStart=/usr/local/bin/docker-compose up -d +ExecStop=/usr/local/bin/docker-compose down +ExecReload=/usr/local/bin/docker-compose restart +TimeoutStartSec=300 +TimeoutStopSec=120 + +# Reiniciar en caso de fallo +Restart=on-failure +RestartSec=30 + +[Install] +WantedBy=multi-user.target +EOF + + # Recargar systemd y habilitar servicio + sudo systemctl daemon-reload + sudo systemctl enable "$service_name" + + echo "✅ Servicio systemd configurado: $service_name" + echo " Comandos disponibles:" + echo " sudo systemctl start $service_name" + echo " sudo systemctl stop $service_name" + echo " sudo systemctl status $service_name" +} + +# Función para configurar inicio automático en WSL2 +setup_wsl_autostart() { + echo "🚀 Configurando inicio automático en WSL2..." + + local wsl_config="$HOME/.profile" + local service_name="industrial-nat" + + # Agregar comando de inicio a .profile si no existe + if ! grep -q "industrial-nat" "$wsl_config" 2>/dev/null; then + cat >> "$wsl_config" << EOF + +# Industrial NAT Service - Auto start +if command -v systemctl &> /dev/null; then + if ! systemctl is-active --quiet $service_name; then + echo "🏭 Iniciando servicio Industrial NAT..." + sudo systemctl start $service_name + fi +fi +EOF + echo "✅ Inicio automático configurado en WSL2" + else + echo "✅ Inicio automático ya estaba configurado" + fi +} + +# Función para iniciar el servicio +start_service() { + echo "🎬 Iniciando servicio Industrial NAT..." + + # Parar contenedor existente si está ejecutándose + docker-compose down 2>/dev/null || true + + # Iniciar servicio + if docker-compose up -d; then + echo "✅ Servicio iniciado exitosamente" + + # Esperar a que esté listo + echo "⏳ Esperando que el servicio esté listo..." + for i in {1..30}; do + if curl -s http://localhost:8080/status >/dev/null 2>&1; then + echo "✅ Servicio está respondiendo" + break + fi + sleep 2 + echo -n "." + done + echo + + else + echo "❌ Error iniciando servicio" + return 1 + fi +} + +# Función para mostrar estado del servicio +show_service_status() { + echo "" + echo "📊 Estado del Servicio Industrial NAT" + echo "====================================" + + # Estado del contenedor + echo "🐳 Estado del contenedor:" + docker-compose ps + + echo "" + echo "📋 Estado de la API:" + if curl -s http://localhost:8080/status | python3 -m json.tool 2>/dev/null; then + echo "✅ API funcionando correctamente" + else + echo "❌ API no responde" + fi + + echo "" + echo "📝 Logs recientes:" + docker-compose logs --tail=10 +} + +# Función para crear scripts de gestión +create_management_scripts() { + echo "📄 Creando scripts de gestión..." + + # Script de inicio rápido + cat > start_nat.sh << 'EOF' +#!/bin/bash +echo "🏭 Iniciando Sistema NAT Industrial..." +cd "$(dirname "$0")" +docker-compose up -d +echo "✅ Sistema iniciado. API disponible en: http://localhost:8080" +EOF + chmod +x start_nat.sh + + # Script de parada + cat > stop_nat.sh << 'EOF' +#!/bin/bash +echo "🛑 Deteniendo Sistema NAT Industrial..." +cd "$(dirname "$0")" +docker-compose down +echo "✅ Sistema detenido" +EOF + chmod +x stop_nat.sh + + # Script de estado + cat > status_nat.sh << 'EOF' +#!/bin/bash +echo "📊 Estado del Sistema NAT Industrial:" +cd "$(dirname "$0")" +docker-compose ps +echo "" +echo "📋 Estado de la API:" +curl -s http://localhost:8080/status | python3 -m json.tool 2>/dev/null || echo "API no disponible" +EOF + chmod +x status_nat.sh + + echo "✅ Scripts de gestión creados:" + echo " ./start_nat.sh - Iniciar sistema" + echo " ./stop_nat.sh - Detener sistema" + echo " ./status_nat.sh - Ver estado" +} + +# Función principal +main() { + echo "Configurando sistema NAT industrial permanente..." + echo "" + + # Verificaciones previas + check_docker + create_directories + test_ssh_connection + + # Construcción e instalación + build_image + create_systemd_service + setup_wsl_autostart + create_management_scripts + + # Iniciar servicio + start_service + + # Mostrar estado + show_service_status + + echo "" + echo "🎉 ¡Sistema NAT Industrial configurado como servicio permanente!" + echo "" + echo "📋 INFORMACIÓN IMPORTANTE:" + echo "=========================" + echo "✅ Servicio: industrial-nat" + echo "✅ Auto-inicio: Configurado para WSL2" + echo "✅ API REST: http://localhost:8080" + echo "✅ Logs: ./logs/nat_proxy.log" + echo "" + echo "🎮 COMANDOS DE GESTIÓN:" + echo "======================" + echo "sudo systemctl start industrial-nat # Iniciar servicio" + echo "sudo systemctl stop industrial-nat # Detener servicio" + echo "sudo systemctl status industrial-nat # Ver estado" + echo "sudo systemctl restart industrial-nat # Reiniciar servicio" + echo "" + echo "⚡ SCRIPTS RÁPIDOS:" + echo "==================" + echo "./start_nat.sh # Inicio rápido" + echo "./stop_nat.sh # Parada rápida" + echo "./status_nat.sh # Ver estado" + echo "" + echo "📱 USO DESDE PC2:" + echo "=================" + echo "python nat_client.py plc 10.1.33.11 vnc --wait" + echo "./scripts/industrial_manager.sh" + echo "" + echo "🔄 El sistema se reiniciará automáticamente:" + echo "- Si WSL2 se reinicia" + echo "- Si el contenedor falla" + echo "- Si PC1 se reinicia" + echo "" +} + +# Ejecutar función principal +main "$@" \ No newline at end of file diff --git a/src/industrial_nat_manager.py b/src/industrial_nat_manager.py new file mode 100644 index 0000000..36363e5 --- /dev/null +++ b/src/industrial_nat_manager.py @@ -0,0 +1,601 @@ +import asyncio +import socket +import logging +import json +import yaml +import time +import signal +import sys +import os +import subprocess +import threading +from typing import Dict, List, Optional, Tuple +from pathlib import Path +from dataclasses import dataclass, asdict + +# Configurar logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('/app/logs/nat_proxy.log'), + logging.StreamHandler(sys.stdout) + ] +) +logger = logging.getLogger(__name__) + +@dataclass +class NATRule: + """Regla de NAT dinámico""" + external_port: int # Puerto expuesto en PC3 + target_ip: str # IP del PLC/dispositivo (ej: 10.1.33.11) + target_port: int # Puerto del PLC/dispositivo + description: str = "" # Descripción opcional + active: bool = True # Si está activa + connections: int = 0 # Conexiones activas + +class DynamicNATProxy: + """Proxy NAT dinámico que puede redirigir a cualquier IP:Puerto""" + + def __init__(self, listen_port: int, target_ip: str, target_port: int, rule_id: str): + self.listen_port = listen_port + self.target_ip = target_ip + self.target_port = target_port + self.rule_id = rule_id + self.server = None + self.connections = [] + self.running = False + self.connection_count = 0 + + async def handle_client(self, client_reader, client_writer): + """Maneja conexiones de clientes y las redirige al objetivo""" + client_addr = client_writer.get_extra_info('peername') + self.connection_count += 1 + conn_id = f"{self.rule_id}-{self.connection_count}" + + logger.info(f"[{conn_id}] Nueva conexión desde {client_addr} -> {self.target_ip}:{self.target_port}") + + target_reader = None + target_writer = None + + try: + # Conectar al objetivo (PLC/SCADA) + target_reader, target_writer = await asyncio.open_connection( + self.target_ip, self.target_port + ) + + logger.info(f"[{conn_id}] Conectado al objetivo {self.target_ip}:{self.target_port}") + + # Crear tareas para reenviar datos en ambas direcciones + task1 = asyncio.create_task( + self.forward_data(client_reader, target_writer, f"{conn_id} client->target") + ) + task2 = asyncio.create_task( + self.forward_data(target_reader, client_writer, f"{conn_id} target->client") + ) + + self.connections.append((task1, task2, client_writer, target_writer, conn_id)) + + # Esperar hasta que una conexión termine + done, pending = await asyncio.wait( + [task1, task2], return_when=asyncio.FIRST_COMPLETED + ) + + # Cancelar tareas pendientes + for task in pending: + task.cancel() + + except Exception as e: + logger.error(f"[{conn_id}] Error en conexión: {e}") + finally: + # Limpiar conexiones + if client_writer: + client_writer.close() + await client_writer.wait_closed() + if target_writer: + target_writer.close() + await target_writer.wait_closed() + + # Remover de lista de conexiones + self.connections = [ + conn for conn in self.connections + if conn[4] != conn_id + ] + + logger.info(f"[{conn_id}] Conexión cerrada") + + async def forward_data(self, reader, writer, direction): + """Reenvía datos entre conexiones con logging""" + try: + total_bytes = 0 + while True: + data = await reader.read(4096) + if not data: + break + writer.write(data) + await writer.drain() + total_bytes += len(data) + + if total_bytes > 0: + logger.debug(f"{direction}: {total_bytes} bytes transferidos") + + except Exception as e: + logger.debug(f"Error en {direction}: {e}") + + async def start(self): + """Inicia el proxy NAT""" + try: + self.server = await asyncio.start_server( + self.handle_client, + '0.0.0.0', + self.listen_port + ) + self.running = True + + addr = self.server.sockets[0].getsockname() + logger.info(f"NAT Proxy iniciado: {addr[0]}:{addr[1]} -> {self.target_ip}:{self.target_port}") + + async with self.server: + await self.server.serve_forever() + + except Exception as e: + logger.error(f"Error iniciando NAT proxy en puerto {self.listen_port}: {e}") + self.running = False + + async def stop(self): + """Detiene el proxy NAT""" + if self.server: + self.server.close() + await self.server.wait_closed() + self.running = False + + # Cerrar todas las conexiones activas + for task1, task2, client_writer, target_writer, conn_id in self.connections: + task1.cancel() + task2.cancel() + client_writer.close() + target_writer.close() + + self.connections.clear() + logger.info(f"NAT Proxy detenido (puerto {self.listen_port})") + +class SSHTunnel: + """Maneja túneles SSH reversos hacia PC3""" + + def __init__(self, local_port: int, remote_port: int, ssh_host: str, + ssh_user: str, ssh_key_file: str, ssh_port: int = 22): + self.local_port = local_port + self.remote_port = remote_port + self.ssh_host = ssh_host + self.ssh_user = ssh_user + self.ssh_key_file = ssh_key_file + self.ssh_port = ssh_port + self.process = None + self.running = False + + def start_reverse_tunnel(self): + """Inicia túnel SSH reverso a PC3""" + try: + cmd = [ + 'ssh', + '-N', # No ejecutar comando remoto + '-R', f'{self.remote_port}:localhost:{self.local_port}', # Túnel reverso + '-o', 'StrictHostKeyChecking=no', + '-o', 'UserKnownHostsFile=/dev/null', + '-o', 'ServerAliveInterval=30', + '-o', 'ServerAliveCountMax=3', + '-o', 'ExitOnForwardFailure=yes', + '-i', self.ssh_key_file, + '-p', str(self.ssh_port), + f'{self.ssh_user}@{self.ssh_host}' + ] + + logger.info(f"Iniciando túnel SSH: PC3:{self.remote_port} <- WSL2:{self.local_port}") + + self.process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + preexec_fn=os.setsid + ) + + self.running = True + + # Monitorear en hilo separado + def monitor(): + try: + self.process.wait() + self.running = False + if self.process.returncode != 0: + stderr = self.process.stderr.read().decode() + logger.error(f"Túnel SSH falló: {stderr}") + except Exception as e: + logger.error(f"Error monitoreando túnel: {e}") + self.running = False + + threading.Thread(target=monitor, daemon=True).start() + + # Verificar establecimiento del túnel + time.sleep(3) + if not self.running or self.process.poll() is not None: + raise Exception("No se pudo establecer el túnel SSH") + + return True + + except Exception as e: + logger.error(f"Error iniciando túnel SSH: {e}") + self.running = False + return False + + def stop(self): + """Detiene el túnel SSH""" + if self.process and self.running: + try: + os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) + self.process.wait(timeout=5) + except subprocess.TimeoutExpired: + os.killpg(os.getpgid(self.process.pid), signal.SIGKILL) + except Exception as e: + logger.error(f"Error deteniendo túnel SSH: {e}") + + self.running = False + logger.info(f"Túnel SSH detenido") + +class IndustrialNATManager: + """Gestor principal del sistema NAT industrial""" + + def __init__(self, config_file: str = '/app/config/nat_config.yaml'): + self.config_file = config_file + self.nat_rules: Dict[int, NATRule] = {} + self.nat_proxies: Dict[int, DynamicNATProxy] = {} + self.ssh_tunnels: Dict[int, SSHTunnel] = {} + self.config = {} + self.running = False + self.next_port = 9000 # Puerto base para asignación automática + self.load_config() + + def load_config(self): + """Carga configuración desde YAML""" + try: + with open(self.config_file, 'r') as f: + self.config = yaml.safe_load(f) + logger.info(f"Configuración cargada desde {self.config_file}") + except FileNotFoundError: + logger.warning("Archivo de configuración no encontrado, creando configuración por defecto") + self.create_default_config() + except Exception as e: + logger.error(f"Error cargando configuración: {e}") + self.create_default_config() + + def create_default_config(self): + """Crea configuración por defecto""" + self.config = { + 'ssh_server': { + 'host': '91.99.210.72', + 'port': 22, + 'user': 'root', + 'key_file': '/app/certs/ssh_private_key' + }, + 'nat_rules': [ + { + 'external_port': 9001, + 'target_ip': '10.1.33.11', + 'target_port': 5900, + 'description': 'PLC VNC Access', + 'active': True + } + ], + 'management': { + 'port': 8080, + 'enabled': True + }, + 'auto_port_range': { + 'start': 9000, + 'end': 9999 + } + } + self.save_config() + + def save_config(self): + """Guarda configuración actual""" + try: + Path(self.config_file).parent.mkdir(parents=True, exist_ok=True) + + # Actualizar reglas NAT en configuración + self.config['nat_rules'] = [ + asdict(rule) for rule in self.nat_rules.values() + ] + + with open(self.config_file, 'w') as f: + yaml.dump(self.config, f, default_flow_style=False) + logger.info("Configuración guardada") + except Exception as e: + logger.error(f"Error guardando configuración: {e}") + + def get_next_available_port(self) -> int: + """Obtiene el siguiente puerto disponible""" + start_port = self.config['auto_port_range']['start'] + end_port = self.config['auto_port_range']['end'] + + for port in range(start_port, end_port + 1): + if port not in self.nat_rules: + return port + + raise Exception("No hay puertos disponibles en el rango configurado") + + async def add_nat_rule(self, target_ip: str, target_port: int, + external_port: Optional[int] = None, + description: str = "") -> Dict: + """Añade nueva regla NAT dinámicamente""" + + if external_port is None: + external_port = self.get_next_available_port() + + if external_port in self.nat_rules: + return { + 'success': False, + 'error': f'Puerto {external_port} ya está en uso' + } + + try: + # Crear regla NAT + rule = NATRule( + external_port=external_port, + target_ip=target_ip, + target_port=target_port, + description=description or f"NAT to {target_ip}:{target_port}" + ) + + # Crear proxy NAT + rule_id = f"nat-{external_port}" + nat_proxy = DynamicNATProxy( + listen_port=external_port, + target_ip=target_ip, + target_port=target_port, + rule_id=rule_id + ) + + # Crear túnel SSH + ssh_config = self.config['ssh_server'] + ssh_tunnel = SSHTunnel( + local_port=external_port, + remote_port=external_port, + ssh_host=ssh_config['host'], + ssh_user=ssh_config['user'], + ssh_key_file=ssh_config['key_file'], + ssh_port=ssh_config.get('port', 22) + ) + + # Iniciar proxy NAT + proxy_task = asyncio.create_task(nat_proxy.start()) + await asyncio.sleep(1) # Esperar que el proxy esté listo + + # Iniciar túnel SSH + if ssh_tunnel.start_reverse_tunnel(): + self.nat_rules[external_port] = rule + self.nat_proxies[external_port] = nat_proxy + self.ssh_tunnels[external_port] = ssh_tunnel + + self.save_config() + + logger.info(f"Regla NAT creada: PC3:{external_port} -> {target_ip}:{target_port}") + + return { + 'success': True, + 'external_port': external_port, + 'target_ip': target_ip, + 'target_port': target_port, + 'access_url': f"{ssh_config['host']}:{external_port}", + 'description': rule.description + } + else: + await nat_proxy.stop() + return { + 'success': False, + 'error': 'No se pudo establecer túnel SSH' + } + + except Exception as e: + logger.error(f"Error añadiendo regla NAT: {e}") + return { + 'success': False, + 'error': str(e) + } + + async def remove_nat_rule(self, external_port: int) -> Dict: + """Elimina regla NAT""" + if external_port not in self.nat_rules: + return { + 'success': False, + 'error': f'No existe regla para puerto {external_port}' + } + + try: + # Detener túnel SSH + if external_port in self.ssh_tunnels: + self.ssh_tunnels[external_port].stop() + del self.ssh_tunnels[external_port] + + # Detener proxy NAT + if external_port in self.nat_proxies: + await self.nat_proxies[external_port].stop() + del self.nat_proxies[external_port] + + # Eliminar regla + rule = self.nat_rules[external_port] + del self.nat_rules[external_port] + + self.save_config() + + logger.info(f"Regla NAT eliminada: puerto {external_port}") + + return { + 'success': True, + 'message': f'Regla NAT eliminada: {rule.target_ip}:{rule.target_port}' + } + + except Exception as e: + logger.error(f"Error eliminando regla NAT: {e}") + return { + 'success': False, + 'error': str(e) + } + + def get_status(self) -> Dict: + """Obtiene estado completo del sistema""" + status = { + 'ssh_server': self.config['ssh_server']['host'], + 'total_rules': len(self.nat_rules), + 'active_connections': 0, + 'rules': [] + } + + for port, rule in self.nat_rules.items(): + proxy = self.nat_proxies.get(port) + tunnel = self.ssh_tunnels.get(port) + active_connections = len(proxy.connections) if proxy else 0 + + status['active_connections'] += active_connections + + status['rules'].append({ + 'external_port': rule.external_port, + 'target_ip': rule.target_ip, + 'target_port': rule.target_port, + 'description': rule.description, + 'active': rule.active, + 'proxy_running': proxy.running if proxy else False, + 'tunnel_running': tunnel.running if tunnel else False, + 'active_connections': active_connections, + 'access_url': f"{self.config['ssh_server']['host']}:{rule.external_port}" + }) + + return status + + async def start_management_server(self): + """Inicia servidor de gestión web""" + if not self.config['management']['enabled']: + return + + from aiohttp import web + + async def handle_status(request): + return web.json_response(self.get_status()) + + async def handle_add_rule(request): + try: + data = await request.json() + result = await self.add_nat_rule( + target_ip=data['target_ip'], + target_port=int(data['target_port']), + external_port=data.get('external_port'), + description=data.get('description', '') + ) + return web.json_response(result) + except Exception as e: + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=400) + + async def handle_remove_rule(request): + try: + data = await request.json() + result = await self.remove_nat_rule(int(data['external_port'])) + return web.json_response(result) + except Exception as e: + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=400) + + async def handle_quick_connect(request): + """Endpoint para conexión rápida (asigna puerto automáticamente)""" + try: + data = await request.json() + result = await self.add_nat_rule( + target_ip=data['target_ip'], + target_port=int(data['target_port']), + description=data.get('description', f"Quick connect to {data['target_ip']}:{data['target_port']}") + ) + return web.json_response(result) + except Exception as e: + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=400) + + app = web.Application() + app.router.add_get('/status', handle_status) + app.router.add_post('/add', handle_add_rule) + app.router.add_post('/remove', handle_remove_rule) + app.router.add_post('/quick-connect', handle_quick_connect) + + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, '0.0.0.0', self.config['management']['port']) + await site.start() + + logger.info(f"Servidor de gestión iniciado en puerto {self.config['management']['port']}") + + async def start_existing_rules(self): + """Inicia reglas NAT existentes en la configuración""" + for rule_config in self.config.get('nat_rules', []): + if rule_config.get('active', True): + await self.add_nat_rule( + target_ip=rule_config['target_ip'], + target_port=rule_config['target_port'], + external_port=rule_config['external_port'], + description=rule_config.get('description', '') + ) + + async def run(self): + """Ejecuta el gestor NAT industrial""" + self.running = True + logger.info("Iniciando Industrial NAT Manager...") + + def signal_handler(signum, frame): + logger.info("Señal de interrupción recibida") + self.running = False + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + try: + # Iniciar servidor de gestión + await self.start_management_server() + + # Iniciar reglas existentes + await self.start_existing_rules() + + logger.info("Industrial NAT Manager iniciado correctamente") + + # Mantener proceso activo + while self.running: + await asyncio.sleep(1) + + except Exception as e: + logger.error(f"Error en Industrial NAT Manager: {e}") + finally: + # Limpiar recursos + for tunnel in self.ssh_tunnels.values(): + tunnel.stop() + for proxy in self.nat_proxies.values(): + await proxy.stop() + logger.info("Industrial NAT Manager detenido") + + +if __name__ == "__main__": + # Crear directorios necesarios + Path('/app/logs').mkdir(parents=True, exist_ok=True) + Path('/app/config').mkdir(parents=True, exist_ok=True) + Path('/app/certs').mkdir(parents=True, exist_ok=True) + + manager = IndustrialNATManager() + + try: + asyncio.run(manager.run()) + except KeyboardInterrupt: + logger.info("Proceso interrumpido por el usuario") + except Exception as e: + logger.error(f"Error fatal: {e}") + sys.exit(1) \ No newline at end of file diff --git a/src/proxy_manager.py b/src/proxy_manager.py new file mode 100644 index 0000000..f51dc32 --- /dev/null +++ b/src/proxy_manager.py @@ -0,0 +1,382 @@ +import asyncio +import ssl +import socket +import logging +import json +import yaml +import time +from typing import Dict, List, Optional, Tuple +from pathlib import Path +import signal +import sys + +# Configurar logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('/app/logs/proxy.log'), + logging.StreamHandler(sys.stdout) + ] +) +logger = logging.getLogger(__name__) + +class TCPProxy: + def __init__(self, local_port: int, remote_host: str, remote_port: int, + cert_file: Optional[str] = None, key_file: Optional[str] = None, + ca_file: Optional[str] = None, use_ssl: bool = False): + self.local_port = local_port + self.remote_host = remote_host + self.remote_port = remote_port + self.cert_file = cert_file + self.key_file = key_file + self.ca_file = ca_file + self.use_ssl = use_ssl + self.server = None + self.connections = [] + self.running = False + + async def handle_client(self, client_reader, client_writer): + """Maneja las conexiones de clientes locales""" + client_addr = client_writer.get_extra_info('peername') + logger.info(f"Nueva conexión desde {client_addr} al puerto {self.local_port}") + + try: + # Crear conexión SSL al servidor remoto si es necesario + if self.use_ssl: + context = ssl.create_default_context() + if self.ca_file: + context.load_verify_locations(self.ca_file) + if self.cert_file and self.key_file: + context.load_cert_chain(self.cert_file, self.key_file) + + remote_reader, remote_writer = await asyncio.open_connection( + self.remote_host, self.remote_port, ssl=context + ) + else: + remote_reader, remote_writer = await asyncio.open_connection( + self.remote_host, self.remote_port + ) + + logger.info(f"Conectado al servidor remoto {self.remote_host}:{self.remote_port}") + + # Crear tareas para reenviar datos en ambas direcciones + task1 = asyncio.create_task( + self.forward_data(client_reader, remote_writer, "client->remote") + ) + task2 = asyncio.create_task( + self.forward_data(remote_reader, client_writer, "remote->client") + ) + + self.connections.append((task1, task2, client_writer, remote_writer)) + + # Esperar a que termine alguna de las conexiones + done, pending = await asyncio.wait( + [task1, task2], return_when=asyncio.FIRST_COMPLETED + ) + + # Cancelar tareas pendientes + for task in pending: + task.cancel() + + except Exception as e: + logger.error(f"Error en conexión desde {client_addr}: {e}") + finally: + client_writer.close() + if 'remote_writer' in locals(): + remote_writer.close() + logger.info(f"Conexión cerrada: {client_addr}") + + async def forward_data(self, reader, writer, direction): + """Reenvía datos entre conexiones""" + try: + while True: + data = await reader.read(4096) + if not data: + break + writer.write(data) + await writer.drain() + logger.debug(f"Datos reenviados ({direction}): {len(data)} bytes") + except Exception as e: + logger.debug(f"Error reenviando datos ({direction}): {e}") + + async def start(self): + """Inicia el servidor proxy""" + try: + self.server = await asyncio.start_server( + self.handle_client, + '0.0.0.0', + self.local_port + ) + self.running = True + + addr = self.server.sockets[0].getsockname() + logger.info(f"Proxy TCP iniciado en {addr[0]}:{addr[1]} -> {self.remote_host}:{self.remote_port}") + + async with self.server: + await self.server.serve_forever() + + except Exception as e: + logger.error(f"Error iniciando servidor en puerto {self.local_port}: {e}") + self.running = False + + async def stop(self): + """Detiene el servidor proxy""" + if self.server: + self.server.close() + await self.server.wait_closed() + self.running = False + + # Cerrar todas las conexiones activas + for task1, task2, client_writer, remote_writer in self.connections: + task1.cancel() + task2.cancel() + client_writer.close() + remote_writer.close() + + self.connections.clear() + logger.info(f"Proxy en puerto {self.local_port} detenido") + + +class ProxyManager: + def __init__(self, config_file: str = '/app/config/proxy_config.yaml'): + self.config_file = config_file + self.proxies: Dict[int, TCPProxy] = {} + self.config = {} + self.running = False + self.load_config() + + def load_config(self): + """Carga la configuración desde archivo YAML""" + try: + with open(self.config_file, 'r') as f: + self.config = yaml.safe_load(f) + logger.info(f"Configuración cargada desde {self.config_file}") + except FileNotFoundError: + logger.warning(f"Archivo de configuración {self.config_file} no encontrado, usando configuración por defecto") + self.create_default_config() + except Exception as e: + logger.error(f"Error cargando configuración: {e}") + self.create_default_config() + + def create_default_config(self): + """Crea una configuración por defecto""" + self.config = { + 'remote_server': { + 'host': '91.99.210.72', + 'use_ssl': True, + 'cert_file': '/app/certs/client.crt', + 'key_file': '/app/certs/client.key', + 'ca_file': '/app/certs/ca.crt' + }, + 'proxies': [], + 'management': { + 'port': 8080, + 'enabled': True + } + } + self.save_config() + + def save_config(self): + """Guarda la configuración actual""" + try: + Path(self.config_file).parent.mkdir(parents=True, exist_ok=True) + with open(self.config_file, 'w') as f: + yaml.dump(self.config, f, default_flow_style=False) + logger.info("Configuración guardada") + except Exception as e: + logger.error(f"Error guardando configuración: {e}") + + async def add_proxy(self, local_port: int, remote_port: int) -> bool: + """Añade un nuevo proxy""" + if local_port in self.proxies: + logger.warning(f"El puerto {local_port} ya está en uso") + return False + + try: + remote_config = self.config['remote_server'] + proxy = TCPProxy( + local_port=local_port, + remote_host=remote_config['host'], + remote_port=remote_port, + cert_file=remote_config.get('cert_file'), + key_file=remote_config.get('key_file'), + ca_file=remote_config.get('ca_file'), + use_ssl=remote_config.get('use_ssl', False) + ) + + # Iniciar el proxy en una tarea separada + task = asyncio.create_task(proxy.start()) + self.proxies[local_port] = proxy + + # Añadir a la configuración + proxy_config = { + 'local_port': local_port, + 'remote_port': remote_port, + 'enabled': True + } + self.config['proxies'].append(proxy_config) + self.save_config() + + logger.info(f"Proxy añadido: {local_port} -> {remote_config['host']}:{remote_port}") + return True + + except Exception as e: + logger.error(f"Error añadiendo proxy: {e}") + return False + + async def remove_proxy(self, local_port: int) -> bool: + """Elimina un proxy existente""" + if local_port not in self.proxies: + logger.warning(f"No existe proxy en puerto {local_port}") + return False + + try: + await self.proxies[local_port].stop() + del self.proxies[local_port] + + # Eliminar de la configuración + self.config['proxies'] = [ + p for p in self.config['proxies'] + if p['local_port'] != local_port + ] + self.save_config() + + logger.info(f"Proxy eliminado del puerto {local_port}") + return True + + except Exception as e: + logger.error(f"Error eliminando proxy: {e}") + return False + + async def start_management_server(self): + """Inicia el servidor de gestión HTTP""" + if not self.config['management']['enabled']: + return + + from aiohttp import web, web_request + + async def handle_status(request): + """Endpoint para obtener el estado""" + status = { + 'proxies': [], + 'total_connections': sum(len(proxy.connections) for proxy in self.proxies.values()) + } + + for port, proxy in self.proxies.items(): + status['proxies'].append({ + 'local_port': port, + 'remote_host': proxy.remote_host, + 'remote_port': proxy.remote_port, + 'running': proxy.running, + 'connections': len(proxy.connections), + 'use_ssl': proxy.use_ssl + }) + + return web.json_response(status) + + async def handle_add_proxy(request): + """Endpoint para añadir proxy""" + try: + data = await request.json() + local_port = data['local_port'] + remote_port = data['remote_port'] + + success = await self.add_proxy(local_port, remote_port) + + if success: + return web.json_response({'status': 'success', 'message': f'Proxy añadido en puerto {local_port}'}) + else: + return web.json_response({'status': 'error', 'message': 'Error añadiendo proxy'}, status=400) + + except Exception as e: + return web.json_response({'status': 'error', 'message': str(e)}, status=400) + + async def handle_remove_proxy(request): + """Endpoint para eliminar proxy""" + try: + data = await request.json() + local_port = data['local_port'] + + success = await self.remove_proxy(local_port) + + if success: + return web.json_response({'status': 'success', 'message': f'Proxy eliminado del puerto {local_port}'}) + else: + return web.json_response({'status': 'error', 'message': 'Error eliminando proxy'}, status=400) + + except Exception as e: + return web.json_response({'status': 'error', 'message': str(e)}, status=400) + + app = web.Application() + app.router.add_get('/status', handle_status) + app.router.add_post('/add', handle_add_proxy) + app.router.add_post('/remove', handle_remove_proxy) + + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, '0.0.0.0', self.config['management']['port']) + await site.start() + + logger.info(f"Servidor de gestión iniciado en puerto {self.config['management']['port']}") + + async def start_existing_proxies(self): + """Inicia los proxies definidos en la configuración""" + for proxy_config in self.config.get('proxies', []): + if proxy_config.get('enabled', True): + await self.add_proxy( + proxy_config['local_port'], + proxy_config['remote_port'] + ) + + async def run(self): + """Ejecuta el gestor de proxies""" + self.running = True + logger.info("Iniciando ProxyManager...") + + # Configurar manejador de señales + def signal_handler(signum, frame): + logger.info("Señal de interrupción recibida, cerrando...") + self.running = False + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + try: + # Iniciar servidor de gestión + await self.start_management_server() + + # Iniciar proxies existentes + await self.start_existing_proxies() + + logger.info("ProxyManager iniciado correctamente") + + # Mantener el proceso corriendo + while self.running: + await asyncio.sleep(1) + + except Exception as e: + logger.error(f"Error en ProxyManager: {e}") + finally: + # Limpiar recursos + for proxy in self.proxies.values(): + await proxy.stop() + logger.info("ProxyManager detenido") + + +if __name__ == "__main__": + # Crear directorios necesarios + Path('/app/logs').mkdir(parents=True, exist_ok=True) + Path('/app/config').mkdir(parents=True, exist_ok=True) + Path('/app/certs').mkdir(parents=True, exist_ok=True) + + # Ejecutar el gestor + manager = ProxyManager() + + try: + asyncio.run(manager.run()) + except KeyboardInterrupt: + logger.info("Proceso interrumpido por el usuario") + except Exception as e: + logger.error(f"Error fatal: {e}") + sys.exit(1) \ No newline at end of file diff --git a/src/ssh_proxy_manager.py b/src/ssh_proxy_manager.py new file mode 100644 index 0000000..db2235c --- /dev/null +++ b/src/ssh_proxy_manager.py @@ -0,0 +1,485 @@ +import asyncio +import subprocess +import logging +import json +import yaml +import time +import signal +import sys +import os +from typing import Dict, List, Optional, Tuple +from pathlib import Path +import threading + +# Configurar logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('/app/logs/proxy.log'), + logging.StreamHandler(sys.stdout) + ] +) +logger = logging.getLogger(__name__) + +class SSHTunnel: + def __init__(self, local_port: int, remote_port: int, ssh_host: str, + ssh_user: str, ssh_key_file: str, ssh_port: int = 22): + self.local_port = local_port + self.remote_port = remote_port + self.ssh_host = ssh_host + self.ssh_user = ssh_user + self.ssh_key_file = ssh_key_file + self.ssh_port = ssh_port + self.process = None + self.running = False + + def start_reverse_tunnel(self): + """Inicia un túnel SSH reverso""" + try: + # Comando SSH para túnel reverso + # El servidor Linux expondrá remote_port y lo redirigirá a local_port en este contenedor + cmd = [ + 'ssh', + '-N', # No ejecutar comando remoto + '-R', f'{self.remote_port}:localhost:{self.local_port}', # Túnel reverso + '-o', 'StrictHostKeyChecking=no', + '-o', 'UserKnownHostsFile=/dev/null', + '-o', 'ServerAliveInterval=30', + '-o', 'ServerAliveCountMax=3', + '-i', self.ssh_key_file, + '-p', str(self.ssh_port), + f'{self.ssh_user}@{self.ssh_host}' + ] + + logger.info(f"Iniciando túnel SSH reverso: {self.ssh_host}:{self.remote_port} <- localhost:{self.local_port}") + + self.process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + preexec_fn=os.setsid # Para poder matar el grupo de procesos + ) + + self.running = True + logger.info(f"Túnel SSH iniciado con PID {self.process.pid}") + + # Monitorear el proceso en un hilo separado + def monitor(): + try: + self.process.wait() + self.running = False + if self.process.returncode != 0: + logger.error(f"Túnel SSH terminó con código {self.process.returncode}") + except Exception as e: + logger.error(f"Error monitoreando túnel SSH: {e}") + self.running = False + + threading.Thread(target=monitor, daemon=True).start() + + # Esperar un poco para verificar que el túnel se estableció + time.sleep(2) + if not self.running or self.process.poll() is not None: + raise Exception("El túnel SSH no se pudo establecer") + + return True + + except Exception as e: + logger.error(f"Error iniciando túnel SSH: {e}") + self.running = False + return False + + def stop(self): + """Detiene el túnel SSH""" + if self.process and self.running: + try: + # Matar el grupo de procesos + os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) + self.process.wait(timeout=5) + except subprocess.TimeoutExpired: + # Forzar terminación + os.killpg(os.getpgid(self.process.pid), signal.SIGKILL) + except Exception as e: + logger.error(f"Error deteniendo túnel SSH: {e}") + + self.running = False + logger.info(f"Túnel SSH detenido (puerto {self.remote_port})") + +class LocalService: + def __init__(self, port: int, service_type: str = "echo"): + self.port = port + self.service_type = service_type + self.server = None + self.running = False + + async def echo_handler(self, reader, writer): + """Servicio echo simple para pruebas""" + client_addr = writer.get_extra_info('peername') + logger.info(f"Conexión echo desde {client_addr}") + + try: + while True: + data = await reader.read(1024) + if not data: + break + + response = f"ECHO: {data.decode('utf-8', errors='ignore')}" + writer.write(response.encode()) + await writer.drain() + + except Exception as e: + logger.error(f"Error en servicio echo: {e}") + finally: + writer.close() + await writer.wait_closed() + + async def http_handler(self, reader, writer): + """Servicio HTTP simple""" + client_addr = writer.get_extra_info('peername') + logger.info(f"Conexión HTTP desde {client_addr}") + + try: + # Leer request HTTP + request = await reader.read(1024) + request_str = request.decode('utf-8', errors='ignore') + + # Respuesta HTTP simple + response_body = json.dumps({ + "message": "Hello from Docker container!", + "client": str(client_addr), + "timestamp": time.time(), + "request_preview": request_str[:200] + }, indent=2) + + response = ( + "HTTP/1.1 200 OK\r\n" + "Content-Type: application/json\r\n" + "Access-Control-Allow-Origin: *\r\n" + f"Content-Length: {len(response_body)}\r\n" + "\r\n" + f"{response_body}" + ) + + writer.write(response.encode()) + await writer.drain() + + except Exception as e: + logger.error(f"Error en servicio HTTP: {e}") + finally: + writer.close() + await writer.wait_closed() + + async def start(self): + """Inicia el servicio local""" + try: + if self.service_type == "echo": + handler = self.echo_handler + elif self.service_type == "http": + handler = self.http_handler + else: + handler = self.echo_handler + + self.server = await asyncio.start_server( + handler, + '0.0.0.0', + self.port + ) + + self.running = True + logger.info(f"Servicio {self.service_type} iniciado en puerto {self.port}") + + async with self.server: + await self.server.serve_forever() + + except Exception as e: + logger.error(f"Error iniciando servicio en puerto {self.port}: {e}") + self.running = False + + async def stop(self): + """Detiene el servicio""" + if self.server: + self.server.close() + await self.server.wait_closed() + self.running = False + logger.info(f"Servicio detenido en puerto {self.port}") + +class SSHProxyManager: + def __init__(self, config_file: str = '/app/config/proxy_config.yaml'): + self.config_file = config_file + self.tunnels: Dict[int, SSHTunnel] = {} + self.services: Dict[int, LocalService] = {} + self.config = {} + self.running = False + self.load_config() + + def load_config(self): + """Carga la configuración desde archivo YAML""" + try: + with open(self.config_file, 'r') as f: + self.config = yaml.safe_load(f) + logger.info(f"Configuración cargada desde {self.config_file}") + except FileNotFoundError: + logger.warning(f"Archivo de configuración {self.config_file} no encontrado, usando configuración por defecto") + self.create_default_config() + except Exception as e: + logger.error(f"Error cargando configuración: {e}") + self.create_default_config() + + def create_default_config(self): + """Crea una configuración por defecto para SSH""" + self.config = { + 'ssh_server': { + 'host': '91.99.210.72', + 'port': 22, + 'user': 'root', # Cambiar por tu usuario + 'key_file': '/app/certs/ssh_private_key' + }, + 'services': [], + 'management': { + 'port': 8080, + 'enabled': True + } + } + self.save_config() + + def save_config(self): + """Guarda la configuración actual""" + try: + Path(self.config_file).parent.mkdir(parents=True, exist_ok=True) + with open(self.config_file, 'w') as f: + yaml.dump(self.config, f, default_flow_style=False) + logger.info("Configuración guardada") + except Exception as e: + logger.error(f"Error guardando configuración: {e}") + + async def add_service(self, local_port: int, remote_port: int, service_type: str = "http") -> bool: + """Añade un nuevo servicio con túnel SSH""" + if local_port in self.services: + logger.warning(f"El puerto {local_port} ya está en uso") + return False + + try: + ssh_config = self.config['ssh_server'] + + # Crear servicio local + service = LocalService(local_port, service_type) + + # Crear túnel SSH + tunnel = SSHTunnel( + local_port=local_port, + remote_port=remote_port, + ssh_host=ssh_config['host'], + ssh_user=ssh_config['user'], + ssh_key_file=ssh_config['key_file'], + ssh_port=ssh_config.get('port', 22) + ) + + # Iniciar servicio local + service_task = asyncio.create_task(service.start()) + + # Esperar un poco para que el servicio esté listo + await asyncio.sleep(1) + + # Iniciar túnel SSH + if tunnel.start_reverse_tunnel(): + self.services[local_port] = service + self.tunnels[local_port] = tunnel + + # Añadir a la configuración + service_config = { + 'local_port': local_port, + 'remote_port': remote_port, + 'service_type': service_type, + 'enabled': True + } + self.config['services'].append(service_config) + self.save_config() + + logger.info(f"Servicio añadido: localhost:{local_port} -> {ssh_config['host']}:{remote_port}") + return True + else: + await service.stop() + return False + + except Exception as e: + logger.error(f"Error añadiendo servicio: {e}") + return False + + async def remove_service(self, local_port: int) -> bool: + """Elimina un servicio y su túnel SSH""" + if local_port not in self.services: + logger.warning(f"No existe servicio en puerto {local_port}") + return False + + try: + # Detener túnel SSH + if local_port in self.tunnels: + self.tunnels[local_port].stop() + del self.tunnels[local_port] + + # Detener servicio local + await self.services[local_port].stop() + del self.services[local_port] + + # Eliminar de la configuración + self.config['services'] = [ + s for s in self.config['services'] + if s['local_port'] != local_port + ] + self.save_config() + + logger.info(f"Servicio eliminado del puerto {local_port}") + return True + + except Exception as e: + logger.error(f"Error eliminando servicio: {e}") + return False + + async def start_management_server(self): + """Inicia el servidor de gestión HTTP""" + if not self.config['management']['enabled']: + return + + from aiohttp import web + + async def handle_status(request): + """Endpoint para obtener el estado""" + status = { + 'services': [], + 'ssh_server': self.config['ssh_server']['host'] + } + + for port, service in self.services.items(): + tunnel = self.tunnels.get(port) + status['services'].append({ + 'local_port': port, + 'remote_port': tunnel.remote_port if tunnel else None, + 'service_type': service.service_type, + 'service_running': service.running, + 'tunnel_running': tunnel.running if tunnel else False, + 'ssh_host': tunnel.ssh_host if tunnel else None + }) + + return web.json_response(status) + + async def handle_add_service(request): + """Endpoint para añadir servicio""" + try: + data = await request.json() + local_port = data['local_port'] + remote_port = data['remote_port'] + service_type = data.get('service_type', 'http') + + success = await self.add_service(local_port, remote_port, service_type) + + if success: + return web.json_response({ + 'status': 'success', + 'message': f'Servicio {service_type} añadido en puerto {local_port}' + }) + else: + return web.json_response({ + 'status': 'error', + 'message': 'Error añadiendo servicio' + }, status=400) + + except Exception as e: + return web.json_response({'status': 'error', 'message': str(e)}, status=400) + + async def handle_remove_service(request): + """Endpoint para eliminar servicio""" + try: + data = await request.json() + local_port = data['local_port'] + + success = await self.remove_service(local_port) + + if success: + return web.json_response({ + 'status': 'success', + 'message': f'Servicio eliminado del puerto {local_port}' + }) + else: + return web.json_response({ + 'status': 'error', + 'message': 'Error eliminando servicio' + }, status=400) + + except Exception as e: + return web.json_response({'status': 'error', 'message': str(e)}, status=400) + + app = web.Application() + app.router.add_get('/status', handle_status) + app.router.add_post('/add', handle_add_service) + app.router.add_post('/remove', handle_remove_service) + + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, '0.0.0.0', self.config['management']['port']) + await site.start() + + logger.info(f"Servidor de gestión iniciado en puerto {self.config['management']['port']}") + + async def start_existing_services(self): + """Inicia los servicios definidos en la configuración""" + for service_config in self.config.get('services', []): + if service_config.get('enabled', True): + await self.add_service( + service_config['local_port'], + service_config['remote_port'], + service_config.get('service_type', 'http') + ) + + async def run(self): + """Ejecuta el gestor de servicios SSH""" + self.running = True + logger.info("Iniciando SSH ProxyManager...") + + # Configurar manejador de señales + def signal_handler(signum, frame): + logger.info("Señal de interrupción recibida, cerrando...") + self.running = False + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + try: + # Iniciar servidor de gestión + await self.start_management_server() + + # Iniciar servicios existentes + await self.start_existing_services() + + logger.info("SSH ProxyManager iniciado correctamente") + + # Mantener el proceso corriendo + while self.running: + await asyncio.sleep(1) + + except Exception as e: + logger.error(f"Error en SSH ProxyManager: {e}") + finally: + # Limpiar recursos + for tunnel in self.tunnels.values(): + tunnel.stop() + for service in self.services.values(): + await service.stop() + logger.info("SSH ProxyManager detenido") + + +if __name__ == "__main__": + # Crear directorios necesarios + Path('/app/logs').mkdir(parents=True, exist_ok=True) + Path('/app/config').mkdir(parents=True, exist_ok=True) + Path('/app/certs').mkdir(parents=True, exist_ok=True) + + # Ejecutar el gestor SSH + manager = SSHProxyManager() + + try: + asyncio.run(manager.run()) + except KeyboardInterrupt: + logger.info("Proceso interrumpido por el usuario") + except Exception as e: + logger.error(f"Error fatal: {e}") + sys.exit(1) \ No newline at end of file