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