Nuevo mensaje del commit
This commit is contained in:
commit
42b5ef099d
|
@ -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.*
|
|
@ -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"]
|
|
@ -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.
|
|
@ -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!
|
|
@ -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.
|
|
@ -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-----
|
|
@ -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-----
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 "$@"
|
|
@ -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 "$@"
|
|
@ -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
|
|
@ -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())
|
|
@ -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())
|
|
@ -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"
|
|
@ -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 "$@"
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
Loading…
Reference in New Issue