|
|
@@ -1,268 +1,233 @@
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
# ==============================================================================
|
|
|
-# SCRIPT DE PROXY MULTIFILAMENTADO CON RESPUESTAS ROTATIVAS
|
|
|
+# PROXY MULTIFILAMENTADO DE ALTA DISPONIBILIDAD - VERSIÓN PYTHON 3
|
|
|
#
|
|
|
-# - Basado en la versión robusta y estable.
|
|
|
-# - Añade rotación dinámica del mensaje de estado en la respuesta HTTP 101.
|
|
|
-# - Utiliza un iterador cíclico para cambiar el mensaje en cada nueva conexión.
|
|
|
-#
|
|
|
-# Creado por Gemini
|
|
|
+# CARACTERÍSTICAS PRINCIPALES:
|
|
|
+# 1. Rotación de Mensajes: Cada respuesta HTTP 101 varía cíclicamente.
|
|
|
+# 2. Soporte Dual Stack: Escucha en IPv4 (0.0.0.0) e IPv6 (::) simultáneamente.
|
|
|
+# 3. Thread-Safe: Uso de Locks para evitar colisiones entre hilos.
|
|
|
+# 4. Gestión de Logs: Rotación automática para evitar saturación de disco.
|
|
|
+# 5. Rate-Limiting: Protección básica contra ataques de inundación por IP.
|
|
|
# ==============================================================================
|
|
|
+
|
|
|
#screen -dmS badvpn2 /bin/badvpn-udpgw --listen-addr 127.0.0.1:7300 --max-clients 1000 --max-connections-for-client 100
|
|
|
#screen -dmS pydic-80 python3 /root/Pythonv1.py 8080
|
|
|
+
|
|
|
import socket
|
|
|
import threading
|
|
|
import select
|
|
|
import sys
|
|
|
import time
|
|
|
-import os
|
|
|
import logging
|
|
|
import logging.handlers
|
|
|
-import itertools # 💡 Importado para la rotación de mensajes
|
|
|
+import itertools
|
|
|
|
|
|
-# ==============================================================================
|
|
|
-# CONFIGURACIÓN GLOBAL Y SETUP DE LOGGING
|
|
|
-# ==============================================================================
|
|
|
+# --- CONFIGURACIÓN DE RED ---
|
|
|
IPV4_ADDR = '0.0.0.0'
|
|
|
IPV6_ADDR = '::'
|
|
|
+LISTENING_PORT = int(sys.argv[1]) if sys.argv[1:] else 8080
|
|
|
|
|
|
-if sys.argv[1:]:
|
|
|
- LISTENING_PORT = int(sys.argv[1])
|
|
|
-else:
|
|
|
- LISTENING_PORT = 8080
|
|
|
-
|
|
|
-PASS = ''
|
|
|
-PRIORITIZE_IPV4 = True
|
|
|
-CONNECTION_COOLDOWN_TIME = 10
|
|
|
-MAX_CONNECTIONS = 1000
|
|
|
-
|
|
|
-# Constantes de red
|
|
|
-BUFLEN = 4096 * 4
|
|
|
-TIMEOUT = 60
|
|
|
-DEFAULT_HOST = '127.0.0.1:223'
|
|
|
+# --- CONFIGURACIÓN DE SEGURIDAD ---
|
|
|
+MAX_CONNECTIONS = 1000
|
|
|
+CONNECTION_COOLDOWN = 5 # Segundos entre conexiones de la misma IP
|
|
|
+TIMEOUT = 60 # Tiempo de espera para sockets
|
|
|
+BUFLEN = 16384 # 16KB de buffer para mayor velocidad
|
|
|
|
|
|
-# 💡 LISTA DE MENSAJES PARA ROTAR
|
|
|
-# Puedes añadir todos los que quieras aquí.
|
|
|
-MENSAJES_ROTATIVOS = [
|
|
|
+# --- LISTA DE MENSAJES ROTATIVOS ---
|
|
|
+# Se enviarán en la línea de estado de la respuesta HTTP 101
|
|
|
+MENSAJES = [
|
|
|
"Pfsense",
|
|
|
"OPNsense",
|
|
|
"VyOS",
|
|
|
+ "Claro",
|
|
|
+ "Windows Server",
|
|
|
+ "BSD Free",
|
|
|
+ "VyOS",
|
|
|
+ "Altice",
|
|
|
+ "Viva",
|
|
|
+ "Google",
|
|
|
+ "VyOS",
|
|
|
"TNSR"
|
|
|
]
|
|
|
|
|
|
-# Creamos un iterador infinito que rota sobre la lista anterior
|
|
|
-mensaje_iterator = itertools.cycle(MENSAJES_ROTATIVOS)
|
|
|
-# Lock para asegurar que la extracción del siguiente mensaje sea segura entre hilos
|
|
|
-iterator_lock = threading.Lock()
|
|
|
-
|
|
|
-# Configuración del log
|
|
|
-LOG_FILE = '/root/proxy.log'
|
|
|
-MAX_LOG_SIZE = 5 * 1024 * 1024
|
|
|
-BACKUP_COUNT = 5
|
|
|
+# Inicialización del iterador cíclico y lock de seguridad
|
|
|
+mensaje_cycle = itertools.cycle(MENSAJES)
|
|
|
+cycle_lock = threading.Lock()
|
|
|
|
|
|
-def setup_logging():
|
|
|
- log_format = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
|
- file_handler = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes=MAX_LOG_SIZE, backupCount=BACKUP_COUNT)
|
|
|
- file_handler.setFormatter(log_format)
|
|
|
- console_handler = logging.StreamHandler()
|
|
|
- console_handler.setFormatter(log_format)
|
|
|
- logger = logging.getLogger()
|
|
|
+# --- CONFIGURACIÓN DE LOGS ---
|
|
|
+LOG_FILE = 'proxy_server.log'
|
|
|
+def setup_logger():
|
|
|
+ logger = logging.getLogger("ProxyLogger")
|
|
|
logger.setLevel(logging.INFO)
|
|
|
- logger.addHandler(file_handler)
|
|
|
- logger.addHandler(console_handler)
|
|
|
+ formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')
|
|
|
+
|
|
|
+ # Rotación de logs: 5MB por archivo, máximo 3 backups
|
|
|
+ handler = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes=5*1024*1024, backupCount=3)
|
|
|
+ handler.setFormatter(formatter)
|
|
|
+
|
|
|
+ # Salida a consola
|
|
|
+ console = logging.StreamHandler()
|
|
|
+ console.setFormatter(formatter)
|
|
|
+
|
|
|
+ logger.addHandler(handler)
|
|
|
+ logger.addHandler(console)
|
|
|
return logger
|
|
|
|
|
|
-logger = setup_logging()
|
|
|
-
|
|
|
-last_connection_times = {}
|
|
|
-last_connection_lock = threading.Lock()
|
|
|
-connection_limit_semaphore = threading.Semaphore(MAX_CONNECTIONS)
|
|
|
-
|
|
|
-# ==============================================================================
|
|
|
-# CLASE DEL SERVIDOR
|
|
|
-# ==============================================================================
|
|
|
-class Server(threading.Thread):
|
|
|
- def __init__(self, port):
|
|
|
- super().__init__()
|
|
|
- self.running = False
|
|
|
- self.port = port
|
|
|
- self.threads = []
|
|
|
- self.threads_lock = threading.Lock()
|
|
|
- self.ipv4_socket = None
|
|
|
- self.ipv6_socket = None
|
|
|
-
|
|
|
- def run(self):
|
|
|
- logger.info("\n:-------PythonProxy-------:\n")
|
|
|
- logger.info(f"Listening port: {self.port}")
|
|
|
- logger.info(f"Mensajes configurados: {len(MENSAJES_ROTATIVOS)}\n")
|
|
|
- logger.info(":-------------------------:\n")
|
|
|
-
|
|
|
- try:
|
|
|
- self.ipv4_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
- self.ipv4_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
|
- self.ipv4_socket.bind((IPV4_ADDR, self.port))
|
|
|
- self.ipv4_socket.listen(0)
|
|
|
- except socket.error as e:
|
|
|
- logger.error(f"Error IPv4: {e}")
|
|
|
- self.ipv4_socket = None
|
|
|
-
|
|
|
- try:
|
|
|
- self.ipv6_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
|
|
- self.ipv6_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
|
- self.ipv6_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
|
|
- self.ipv6_socket.bind((IPV6_ADDR, self.port, 0, 0))
|
|
|
- self.ipv6_socket.listen(0)
|
|
|
- except socket.error as e:
|
|
|
- logger.error(f"Error IPv6: {e}")
|
|
|
- self.ipv6_socket = None
|
|
|
-
|
|
|
- if not self.ipv4_socket and not self.ipv6_socket:
|
|
|
- return
|
|
|
-
|
|
|
- self.running = True
|
|
|
- active_sockets = [s for s in [self.ipv4_socket, self.ipv6_socket] if s]
|
|
|
-
|
|
|
- try:
|
|
|
- while self.running:
|
|
|
- readable, _, _ = select.select(active_sockets, [], [], 2)
|
|
|
- for sock in readable:
|
|
|
- c, addr = sock.accept()
|
|
|
- client_ip = addr[0]
|
|
|
- current_time = time.time()
|
|
|
-
|
|
|
- with last_connection_lock:
|
|
|
- last_time = last_connection_times.get(client_ip, 0)
|
|
|
- if current_time - last_time < CONNECTION_COOLDOWN_TIME:
|
|
|
- c.close()
|
|
|
- continue
|
|
|
- last_connection_times[client_ip] = current_time
|
|
|
-
|
|
|
- if not connection_limit_semaphore.acquire(timeout=0):
|
|
|
- c.close()
|
|
|
- continue
|
|
|
+log = setup_logger()
|
|
|
+conn_limit = threading.Semaphore(MAX_CONNECTIONS)
|
|
|
+ip_history = {}
|
|
|
+ip_lock = threading.Lock()
|
|
|
|
|
|
- c.setblocking(1)
|
|
|
- conn = ConnectionHandler(c, self, addr)
|
|
|
- conn.start()
|
|
|
- self.add_conn(conn)
|
|
|
- except Exception as e:
|
|
|
- logger.error(f"Error servidor: {e}")
|
|
|
- finally:
|
|
|
- self.running = False
|
|
|
-
|
|
|
- def add_conn(self, conn):
|
|
|
- with self.threads_lock:
|
|
|
- if self.running: self.threads.append(conn)
|
|
|
-
|
|
|
- def remove_conn(self, conn):
|
|
|
- with self.threads_lock:
|
|
|
- if conn in self.threads: self.threads.remove(conn)
|
|
|
-
|
|
|
- def close(self):
|
|
|
- self.running = False
|
|
|
- with self.threads_lock:
|
|
|
- for c in list(self.threads): c.close()
|
|
|
-
|
|
|
-# ==============================================================================
|
|
|
-# CLASE MANEJADORA DE CONEXIONES
|
|
|
-# ==============================================================================
|
|
|
class ConnectionHandler(threading.Thread):
|
|
|
- def __init__(self, client_socket, server, addr):
|
|
|
- super().__init__()
|
|
|
+ def __init__(self, client_socket, addr):
|
|
|
+ super().__init__(daemon=True)
|
|
|
self.client = client_socket
|
|
|
- self.server = server
|
|
|
self.addr = addr
|
|
|
- self.log_prefix = f"{addr[0]}:{addr[1]}"
|
|
|
self.target = None
|
|
|
- self.client_closed = False
|
|
|
- self.target_closed = True
|
|
|
-
|
|
|
- def close(self):
|
|
|
- try:
|
|
|
- if not self.client_closed:
|
|
|
- self.client.close()
|
|
|
- except: pass
|
|
|
- finally: self.client_closed = True
|
|
|
-
|
|
|
- try:
|
|
|
- if not self.target_closed:
|
|
|
- self.target.close()
|
|
|
- except: pass
|
|
|
- finally: self.target_closed = True
|
|
|
-
|
|
|
- connection_limit_semaphore.release()
|
|
|
+ self.log_id = f"{addr[0]}:{addr[1]}"
|
|
|
+
|
|
|
+ def finish(self):
|
|
|
+ """Cierra todos los recursos de la conexión de forma segura."""
|
|
|
+ for s in [self.client, self.target]:
|
|
|
+ if s:
|
|
|
+ try:
|
|
|
+ s.close()
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+ conn_limit.release()
|
|
|
|
|
|
def run(self):
|
|
|
try:
|
|
|
+ # 1. Leer petición inicial
|
|
|
data = self.client.recv(BUFLEN)
|
|
|
- if not data: return
|
|
|
- headers = data.decode('latin-1')
|
|
|
- host_port = self.find_header(headers, 'X-Real-Host') or DEFAULT_HOST
|
|
|
+ if not data:
|
|
|
+ return
|
|
|
|
|
|
- # 💡 Lógica de rotación: obtenemos el siguiente mensaje de la lista
|
|
|
- with iterator_lock:
|
|
|
- mensaje_actual = next(mensaje_iterator)
|
|
|
+ # 2. Determinar destino (Header X-Real-Host o Default)
|
|
|
+ headers = data.decode('latin-1', errors='ignore')
|
|
|
+ target_info = self.extract_header(headers, 'X-Real-Host') or "127.0.0.1:22"
|
|
|
|
|
|
- # Construimos la respuesta HTTP dinámica
|
|
|
- response_dinamica = f"HTTP/1.1 101 {mensaje_actual}\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n".encode()
|
|
|
+ # 3. Obtener mensaje rotativo de forma segura
|
|
|
+ with cycle_lock:
|
|
|
+ msg = next(mensaje_cycle)
|
|
|
|
|
|
- logger.info(f"[{self.log_prefix}] Usando mensaje: '{mensaje_actual}'")
|
|
|
+ # 4. Intentar conectar al destino
|
|
|
+ if not self.connect_to_target(target_info):
|
|
|
+ return
|
|
|
+
|
|
|
+ # 5. Enviar respuesta con el mensaje rotado
|
|
|
+ resp = (f"HTTP/1.1 101 {msg}\r\n"
|
|
|
+ f"Connection: Upgrade\r\n"
|
|
|
+ f"Upgrade: websocket\r\n\r\n").encode('utf-8')
|
|
|
+ self.client.sendall(resp)
|
|
|
|
|
|
- self.connect_target(host_port)
|
|
|
- self.client.sendall(response_dinamica)
|
|
|
- self.do_tunnel()
|
|
|
+ log.info(f"[{self.log_id}] Conectado a {target_info} | Mensaje: {msg}")
|
|
|
+
|
|
|
+ # 6. Iniciar túnel bidireccional
|
|
|
+ self.bridge()
|
|
|
|
|
|
except Exception as e:
|
|
|
- logger.error(f"Error en {self.log_prefix}: {e}")
|
|
|
+ log.error(f"[{self.log_id}] Error: {e}")
|
|
|
finally:
|
|
|
- self.close()
|
|
|
- self.server.remove_conn(self)
|
|
|
+ self.finish()
|
|
|
|
|
|
- def find_header(self, head, header):
|
|
|
- aux = head.find(header + ': ')
|
|
|
- if aux == -1: return ''
|
|
|
- head = head[aux + len(header) + 2:]
|
|
|
- aux = head.find('\r\n')
|
|
|
- return head[:aux]
|
|
|
+ def extract_header(self, text, header_name):
|
|
|
+ for line in text.split('\r\n'):
|
|
|
+ if line.lower().startswith(header_name.lower() + ":"):
|
|
|
+ return line.split(':', 1)[1].strip()
|
|
|
+ return None
|
|
|
|
|
|
- def connect_target(self, host_port):
|
|
|
- i = host_port.find(':')
|
|
|
- host = host_port[:i] if i != -1 else host_port
|
|
|
- port = int(host_port[i+1:]) if i != -1 else 22
|
|
|
-
|
|
|
- addr_info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
|
|
|
- for res in addr_info:
|
|
|
- af, socktype, proto, _, sa = res
|
|
|
- try:
|
|
|
- self.target = socket.socket(af, socktype, proto)
|
|
|
- self.target.connect(sa)
|
|
|
- self.target_closed = False
|
|
|
- return
|
|
|
- except:
|
|
|
- if self.target: self.target.close()
|
|
|
- raise RuntimeError("No se pudo conectar al destino")
|
|
|
-
|
|
|
- def do_tunnel(self):
|
|
|
- socs = [self.client, self.target]
|
|
|
+ def connect_to_target(self, target_str):
|
|
|
+ try:
|
|
|
+ parts = target_str.split(':')
|
|
|
+ host = parts[0]
|
|
|
+ port = int(parts[1]) if len(parts) > 1 else 22
|
|
|
+
|
|
|
+ # Soporte IPv4 e IPv6 automático
|
|
|
+ infos = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
|
|
|
+ for res in infos:
|
|
|
+ af, socktype, proto, canonname, sa = res
|
|
|
+ try:
|
|
|
+ self.target = socket.socket(af, socktype, proto)
|
|
|
+ self.target.settimeout(10)
|
|
|
+ self.target.connect(sa)
|
|
|
+ return True
|
|
|
+ except:
|
|
|
+ continue
|
|
|
+ return False
|
|
|
+ except:
|
|
|
+ return False
|
|
|
+
|
|
|
+ def bridge(self):
|
|
|
+ """Mueve datos entre cliente y destino usando select."""
|
|
|
+ sockets = [self.client, self.target]
|
|
|
while True:
|
|
|
- readable, _, _ = select.select(socs, [], [], TIMEOUT)
|
|
|
- if not readable: break
|
|
|
- for sock in readable:
|
|
|
- data = sock.recv(BUFLEN)
|
|
|
- if not data: return
|
|
|
- if sock is self.target:
|
|
|
- self.client.send(data)
|
|
|
- else:
|
|
|
- self.target.sendall(data)
|
|
|
+ readable, _, error = select.select(sockets, [], sockets, TIMEOUT)
|
|
|
+ if error or not readable:
|
|
|
+ break
|
|
|
+
|
|
|
+ for s in readable:
|
|
|
+ other = self.target if s is self.client else self.client
|
|
|
+ try:
|
|
|
+ chunk = s.recv(BUFLEN)
|
|
|
+ if not chunk:
|
|
|
+ return
|
|
|
+ other.sendall(chunk)
|
|
|
+ except:
|
|
|
+ return
|
|
|
|
|
|
def main():
|
|
|
- server = Server(LISTENING_PORT)
|
|
|
- server.start()
|
|
|
+ # Crear sockets de escucha
|
|
|
+ listeners = []
|
|
|
+ for addr_info in [(socket.AF_INET, IPV4_ADDR), (socket.AF_INET6, IPV6_ADDR)]:
|
|
|
+ try:
|
|
|
+ s = socket.socket(addr_info[0], socket.SOCK_STREAM)
|
|
|
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
|
+ if addr_info[0] == socket.AF_INET6:
|
|
|
+ s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
|
|
+ s.bind((addr_info[1], LISTENING_PORT))
|
|
|
+ s.listen(128)
|
|
|
+ listeners.append(s)
|
|
|
+ log.info(f"Escuchando en {addr_info[1]}:{LISTENING_PORT}")
|
|
|
+ except Exception as e:
|
|
|
+ log.warning(f"No se pudo abrir socket en {addr_info[1]}: {e}")
|
|
|
+
|
|
|
+ if not listeners:
|
|
|
+ log.critical("No hay sockets disponibles. Saliendo.")
|
|
|
+ return
|
|
|
+
|
|
|
+ log.info("Servidor Proxy iniciado. Presiona Ctrl+C para salir.")
|
|
|
+
|
|
|
try:
|
|
|
- while True: time.sleep(2)
|
|
|
+ while True:
|
|
|
+ r, _, _ = select.select(listeners, [], [])
|
|
|
+ for s in r:
|
|
|
+ client, addr = s.accept()
|
|
|
+
|
|
|
+ # Control de frecuencia por IP
|
|
|
+ ip = addr[0]
|
|
|
+ with ip_lock:
|
|
|
+ now = time.time()
|
|
|
+ last = ip_history.get(ip, 0)
|
|
|
+ if now - last < CONNECTION_COOLDOWN:
|
|
|
+ client.close()
|
|
|
+ continue
|
|
|
+ ip_history[ip] = now
|
|
|
+
|
|
|
+ # Control de límite total
|
|
|
+ if not conn_limit.acquire(blocking=False):
|
|
|
+ log.warning(f"Límite de conexiones alcanzado ({MAX_CONNECTIONS})")
|
|
|
+ client.close()
|
|
|
+ continue
|
|
|
+
|
|
|
+ # Iniciar manejador
|
|
|
+ ConnectionHandler(client, addr).start()
|
|
|
except KeyboardInterrupt:
|
|
|
- server.close()
|
|
|
+ log.info("Cerrando servidor...")
|
|
|
+ finally:
|
|
|
+ for s in listeners:
|
|
|
+ s.close()
|
|
|
|
|
|
-if __name__ == '__main__':
|
|
|
+if __name__ == "__main__":
|
|
|
main()
|