Nuevo mensaje del commit

This commit is contained in:
Tu Nombre 2025-09-19 14:31:14 +02:00
commit 42b5ef099d
22 changed files with 4202 additions and 0 deletions

68
.gitignore vendored Normal file
View File

@ -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.*

37
Dockerfile Normal file
View File

@ -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"]

237
INDUSTRIAL_README.md Normal file
View File

@ -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.

191
PC3_SETUP.md Normal file
View File

@ -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!

375
README.md Normal file
View File

@ -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!
```
## <20> **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.

24
certs/README.md Normal file
View File

@ -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-----

50
certs/ssh_private_key Normal file
View File

@ -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-----

View File

@ -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

50
config/nat_config.yaml Normal file
View File

@ -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)

29
config/proxy_config.yaml Normal file
View File

@ -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

59
docker-compose.yml Normal file
View File

@ -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

8
requirements.txt Normal file
View File

@ -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

347
scripts/generate_ssh_key.sh Executable file
View File

@ -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 "$@"

283
scripts/industrial_manager.sh Executable file
View File

@ -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 "$@"

172
scripts/manage_proxy.sh Executable file
View File

@ -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 <puerto_local> <puerto_remoto> - Añadir nuevo proxy"
echo " remove <puerto_local> - 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 <puerto_local> <puerto_remoto>"
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 <puerto_local>"
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

277
scripts/nat_client.py Executable file
View File

@ -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())

144
scripts/proxy_client.py Executable file
View File

@ -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())

71
setup.sh Executable file
View File

@ -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"

311
setup_permanent.sh Executable file
View File

@ -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 "$@"

View File

@ -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)

382
src/proxy_manager.py Normal file
View File

@ -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)

485
src/ssh_proxy_manager.py Normal file
View File

@ -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)