|
|
@@ -0,0 +1,268 @@
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
+
|
|
|
+# ==============================================================================
|
|
|
+# SCRIPT DE PROXY MULTIFILAMENTADO CON RESPUESTAS ROTATIVAS
|
|
|
+#
|
|
|
+# - 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
|
|
|
+# ==============================================================================
|
|
|
+#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 /etc/VPS-MX/protocolo/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
|
|
|
+
|
|
|
+# ==============================================================================
|
|
|
+# CONFIGURACIÓN GLOBAL Y SETUP DE LOGGING
|
|
|
+# ==============================================================================
|
|
|
+IPV4_ADDR = '0.0.0.0'
|
|
|
+IPV6_ADDR = '::'
|
|
|
+
|
|
|
+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'
|
|
|
+
|
|
|
+# 💡 LISTA DE MENSAJES PARA ROTAR
|
|
|
+# Puedes añadir todos los que quieras aquí.
|
|
|
+MENSAJES_ROTATIVOS = [
|
|
|
+ "Pfsense",
|
|
|
+ "OPNsense",
|
|
|
+ "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
|
|
|
+
|
|
|
+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()
|
|
|
+ logger.setLevel(logging.INFO)
|
|
|
+ logger.addHandler(file_handler)
|
|
|
+ logger.addHandler(console_handler)
|
|
|
+ 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
|
|
|
+
|
|
|
+ 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__()
|
|
|
+ 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()
|
|
|
+
|
|
|
+ def run(self):
|
|
|
+ try:
|
|
|
+ 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
|
|
|
+
|
|
|
+ # 💡 Lógica de rotación: obtenemos el siguiente mensaje de la lista
|
|
|
+ with iterator_lock:
|
|
|
+ mensaje_actual = next(mensaje_iterator)
|
|
|
+
|
|
|
+ # 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()
|
|
|
+
|
|
|
+ logger.info(f"[{self.log_prefix}] Usando mensaje: '{mensaje_actual}'")
|
|
|
+
|
|
|
+ self.connect_target(host_port)
|
|
|
+ self.client.sendall(response_dinamica)
|
|
|
+ self.do_tunnel()
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"Error en {self.log_prefix}: {e}")
|
|
|
+ finally:
|
|
|
+ self.close()
|
|
|
+ self.server.remove_conn(self)
|
|
|
+
|
|
|
+ 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 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]
|
|
|
+ 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)
|
|
|
+
|
|
|
+def main():
|
|
|
+ server = Server(LISTENING_PORT)
|
|
|
+ server.start()
|
|
|
+ try:
|
|
|
+ while True: time.sleep(2)
|
|
|
+ except KeyboardInterrupt:
|
|
|
+ server.close()
|
|
|
+
|
|
|
+if __name__ == '__main__':
|
|
|
+ main()
|