|
|
@@ -11,19 +11,22 @@ import os
|
|
|
# --- CONFIGURACIÓN BASE ---
|
|
|
LISTENING_PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
|
|
|
SSH_HOST = '127.0.0.1'
|
|
|
-SSH_PORT = 223 # Compatible con Dropbear o OpenSSH local
|
|
|
+SSH_PORT = 22 # Compatible con Dropbear (22 o 223) y OpenSSH
|
|
|
LOG_FILE = "/root/proxy.log"
|
|
|
MAX_LOG_SIZE = 10 * 1024 * 1024 # 10MB
|
|
|
|
|
|
# --- CONFIGURACIÓN DE SEGURIDAD AVANZADA ---
|
|
|
MAX_CONNECTIONS = 1000
|
|
|
-CONNECTION_COOLDOWN = 0.7
|
|
|
+CONNECTION_COOLDOWN = 0.5
|
|
|
TIMEOUT = 60
|
|
|
BUFLEN = 16384
|
|
|
|
|
|
-# --- FILTRADO DE ACCESO ---
|
|
|
+# --- FILTRADO DE ACCESO Y AUTO-BANEO ---
|
|
|
ALLOWED_IPS = []
|
|
|
BLOCKED_HOSTS = ['ads.doubleclick.net', 'telemetry.microsoft.com']
|
|
|
+AUTO_BAN_STRIKES = 3 # Intentos de flood antes de banear la IP en memoria
|
|
|
+banned_ips_memory = set() # Lista negra dinámica en RAM
|
|
|
+ip_strikes = {}
|
|
|
|
|
|
# --- SECCIÓN DE CUSTOM HEADERS (Inyectados en la respuesta 101) ---
|
|
|
CUSTOM_HEADERS = {
|
|
|
@@ -31,50 +34,35 @@ CUSTOM_HEADERS = {
|
|
|
"X-Forwarded-For": "127.0.0.1",
|
|
|
"Content-Type": "text/html; charset=UTF-8",
|
|
|
"Proxy-Connection": "keep-alive",
|
|
|
- "Cache-Control": "no-cache",
|
|
|
- "X-Real-IP": "127.0.0.1"
|
|
|
+ "Cache-Control": "no-cache"
|
|
|
}
|
|
|
|
|
|
# --- MENSAJES ROTATIVOS (Status 101) ---
|
|
|
MENSAJES = [
|
|
|
"🚀 CONEXION ESTABLECIDA",
|
|
|
"🛡️ SEGURIDAD ACTIVA",
|
|
|
- "Pfsense",
|
|
|
- "OPNsense",
|
|
|
- "VyOS",
|
|
|
- "Claro",
|
|
|
- "Windows Server",
|
|
|
- "BSD Free",
|
|
|
- "VyOS",
|
|
|
- "Altice",
|
|
|
- "Viva",
|
|
|
- "Google",
|
|
|
- "VyOS",
|
|
|
- "TNSR",
|
|
|
- "🔋 OPTIMIZACION",
|
|
|
- "🌐 ACCESO OK"
|
|
|
+ "🔋 OPTIMIZACION SISTEMA",
|
|
|
+ "🌐 ACCESO NETMOD OK"
|
|
|
]
|
|
|
mensaje_cycle = itertools.cycle(MENSAJES)
|
|
|
cycle_lock = threading.Lock()
|
|
|
|
|
|
# --- SISTEMA DE LOGS CON AUTO-LIMPIEZA ---
|
|
|
def log(msg, addr=None):
|
|
|
- # Verificar tamaño del log y limpiar si excede el límite
|
|
|
- if os.path.exists(LOG_FILE) and os.path.getsize(LOG_FILE) > MAX_LOG_SIZE:
|
|
|
- with open(LOG_FILE, 'w') as f:
|
|
|
- f.write(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] LOG REINICIADO (Límite 10MB alcanzado)\n")
|
|
|
-
|
|
|
- timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
- client_info = f" [{addr[0]}]" if addr else ""
|
|
|
- log_entry = f"[{timestamp}]{client_info} {msg}\n"
|
|
|
-
|
|
|
- # Escribir en archivo y consola
|
|
|
try:
|
|
|
+ if os.path.exists(LOG_FILE) and os.path.getsize(LOG_FILE) > MAX_LOG_SIZE:
|
|
|
+ with open(LOG_FILE, 'w') as f:
|
|
|
+ f.write(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] LOG REINICIADO (Límite 10MB alcanzado)\n")
|
|
|
+
|
|
|
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
+ client_info = f" [{addr[0]}]" if addr else ""
|
|
|
+ log_entry = f"[{timestamp}]{client_info} {msg}\n"
|
|
|
+
|
|
|
with open(LOG_FILE, 'a') as f:
|
|
|
f.write(log_entry)
|
|
|
print(log_entry.strip())
|
|
|
- except Exception as e:
|
|
|
- print(f"Error escribiendo log: {e}")
|
|
|
+ except:
|
|
|
+ pass
|
|
|
|
|
|
# --- GESTIÓN DE ESTADO ---
|
|
|
active_connections = 0
|
|
|
@@ -87,6 +75,8 @@ class ConnectionHandler(threading.Thread):
|
|
|
self.client = client_socket
|
|
|
self.addr = addr
|
|
|
self.target = None
|
|
|
+ self.tx_bytes = 0 # Bytes transmitidos (Subida)
|
|
|
+ self.rx_bytes = 0 # Bytes recibidos (Bajada)
|
|
|
|
|
|
def build_http_response(self, status_msg):
|
|
|
headers_str = "".join([f"{k}: {v}\r\n" for k, v in CUSTOM_HEADERS.items()])
|
|
|
@@ -100,47 +90,60 @@ class ConnectionHandler(threading.Thread):
|
|
|
|
|
|
def run(self):
|
|
|
global active_connections
|
|
|
+ client_ip = self.addr[0]
|
|
|
+
|
|
|
try:
|
|
|
- log("--- Nueva conexión recibida ---", self.addr)
|
|
|
-
|
|
|
- # 1. Validación de IP
|
|
|
- if ALLOWED_IPS and self.addr[0] not in ALLOWED_IPS:
|
|
|
+ # 1. Filtro de Auto-Baneo (Evita gasto de CPU en atacantes)
|
|
|
+ if client_ip in banned_ips_memory:
|
|
|
+ return # Cierra silenciosamente
|
|
|
+
|
|
|
+ # 2. Validación de Lista Blanca
|
|
|
+ if ALLOWED_IPS and client_ip not in ALLOWED_IPS:
|
|
|
log("🚫 Conexión rechazada: IP no autorizada", self.addr)
|
|
|
return
|
|
|
|
|
|
- # 2. Control de Rate-Limiting / Cooldown
|
|
|
+ # 3. Control de Rate-Limiting e Inteligencia de Auto-Ban
|
|
|
now = time.time()
|
|
|
- if self.addr[0] in ip_cooldowns and (now - ip_cooldowns[self.addr[0]]) < CONNECTION_COOLDOWN:
|
|
|
- log("⚠️ Conexión rechazada por rate-limiting", self.addr)
|
|
|
+ if client_ip in ip_cooldowns and (now - ip_cooldowns[client_ip]) < CONNECTION_COOLDOWN:
|
|
|
+ # Si viola el cooldown, sumamos un strike
|
|
|
+ ip_strikes[client_ip] = ip_strikes.get(client_ip, 0) + 1
|
|
|
+ if ip_strikes[client_ip] >= AUTO_BAN_STRIKES:
|
|
|
+ banned_ips_memory.add(client_ip)
|
|
|
+ log(f"⛔ IP Baneada en memoria RAM por Flood/Spam", self.addr)
|
|
|
return
|
|
|
- ip_cooldowns[self.addr[0]] = now
|
|
|
-
|
|
|
- # 3. Payload inicial
|
|
|
- self.client.settimeout(10)
|
|
|
- payload = self.client.recv(BUFLEN)
|
|
|
- if not payload:
|
|
|
- return
|
|
|
-
|
|
|
- # 4. Conexión al destino (Dropbear/SSH)
|
|
|
- with cycle_lock:
|
|
|
- current_status = next(mensaje_cycle)
|
|
|
-
|
|
|
+
|
|
|
+ ip_cooldowns[client_ip] = now
|
|
|
+ ip_strikes[client_ip] = 0 # Resetea strikes si conecta legalmente
|
|
|
+
|
|
|
+ # 4. Leer payload inicial de Netmod (Absorción)
|
|
|
+ self.client.settimeout(5)
|
|
|
+ try:
|
|
|
+ payload = self.client.recv(BUFLEN)
|
|
|
+ if not payload: return
|
|
|
+
|
|
|
+ # 💡 Modo Sigilo: Si un navegador intenta acceder (ej. GET / HTTP/1.1) y NO es Netmod.
|
|
|
+ # Puedes quitar el comentario de las 3 líneas de abajo para activarlo si quieres que Netmod sea obligatorio.
|
|
|
+ # if b"Upgrade: websocket" not in payload and b"HTTP/" in payload:
|
|
|
+ # self.client.sendall(b"HTTP/1.1 400 Bad Request\r\nServer: nginx\r\n\r\n")
|
|
|
+ # return
|
|
|
+ except: return
|
|
|
+
|
|
|
+ # 5. Conectar al destino (SSH/Dropbear)
|
|
|
log(f"Intentando conectar a {SSH_HOST}:{SSH_PORT}...", self.addr)
|
|
|
+ self.target = socket.create_connection((SSH_HOST, SSH_PORT), timeout=10)
|
|
|
|
|
|
- # Soporte IPv4/IPv6 para la conexión interna
|
|
|
- self.target = socket.create_connection((SSH_HOST, SSH_PORT), timeout=TIMEOUT)
|
|
|
+ # 6. Elegir mensaje rotativo y responder al cliente
|
|
|
+ with cycle_lock:
|
|
|
+ current_status = next(mensaje_cycle)
|
|
|
|
|
|
- log("Conexión exitosa al destino SSH", self.addr)
|
|
|
-
|
|
|
- # 5. Respuesta HTTP 101
|
|
|
self.client.sendall(self.build_http_response(current_status))
|
|
|
- log(f"✅ Protocolo Upgrade: {current_status}", self.addr)
|
|
|
+ log(f"✅ Túnel activo: {current_status}", self.addr)
|
|
|
|
|
|
- # 6. Túnel bidireccional
|
|
|
+ # 7. Túnel bidireccional puro
|
|
|
self.tunnel()
|
|
|
|
|
|
except Exception as e:
|
|
|
- log(f"❌ Error: {str(e)}", self.addr)
|
|
|
+ log(f"❌ Error: {e}", self.addr)
|
|
|
finally:
|
|
|
with conn_lock:
|
|
|
active_connections -= 1
|
|
|
@@ -152,7 +155,7 @@ class ConnectionHandler(threading.Thread):
|
|
|
sockets = [self.client, self.target]
|
|
|
|
|
|
while True:
|
|
|
- readable, _, error = select.select(sockets, [], sockets, TIMEOUT)
|
|
|
+ readable, _, error = select.select(sockets, [], sockets, 300)
|
|
|
if error or not readable:
|
|
|
break
|
|
|
|
|
|
@@ -162,43 +165,58 @@ class ConnectionHandler(threading.Thread):
|
|
|
if not data:
|
|
|
return
|
|
|
|
|
|
- dest = self.target if s is self.client else self.client
|
|
|
- dest.sendall(data)
|
|
|
+ if s is self.client:
|
|
|
+ self.target.sendall(data)
|
|
|
+ self.tx_bytes += len(data) # Sumar subida
|
|
|
+ else:
|
|
|
+ self.client.sendall(data)
|
|
|
+ self.rx_bytes += len(data) # Sumar bajada
|
|
|
except:
|
|
|
return
|
|
|
|
|
|
def cleanup(self):
|
|
|
+ # Calcula el tráfico total al cerrar la conexión
|
|
|
+ total_mb = (self.tx_bytes + self.rx_bytes) / (1024 * 1024)
|
|
|
+ if total_mb > 0.01: # Solo registrar si hubo tráfico real (> 10KB)
|
|
|
+ log(f"[*] Conexión finalizada. Tráfico consumido: {total_mb:.2f} MB", self.addr)
|
|
|
+
|
|
|
for s in [self.client, self.target]:
|
|
|
if s:
|
|
|
- try:
|
|
|
- s.close()
|
|
|
- except:
|
|
|
- pass
|
|
|
+ try: s.close()
|
|
|
+ except: pass
|
|
|
|
|
|
def main():
|
|
|
global active_connections
|
|
|
|
|
|
- # Configuración de Socket Dual (IPv4 e IPv6)
|
|
|
+ # Intentamos socket dual (IPv6/IPv4)
|
|
|
try:
|
|
|
- if socket.has_dualstack_ipv6():
|
|
|
- server = socket.create_server(('::', LISTENING_PORT), family=socket.AF_INET6, dualstack_ipv6=True)
|
|
|
- else:
|
|
|
- server = socket.create_server(('0.0.0.0', LISTENING_PORT), family=socket.AF_INET)
|
|
|
+ addr_info = socket.getaddrinfo(None, LISTENING_PORT, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
|
|
|
+ addr_info.sort(key=lambda x: x[0] == socket.AF_INET6, reverse=True)
|
|
|
|
|
|
+ af, socktype, proto, canonname, sa = addr_info[0]
|
|
|
+ server = socket.socket(af, socktype, proto)
|
|
|
+ server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
|
+
|
|
|
+ if af == socket.AF_INET6:
|
|
|
+ try: server.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
|
|
+ except: pass
|
|
|
+
|
|
|
+ server.bind(sa)
|
|
|
server.listen(200)
|
|
|
- log(f"🔥 Servidor Robusto Iniciado en Puerto {LISTENING_PORT} (Dual IPv4/IPv6)")
|
|
|
- log(f"🛡️ Logs: {LOG_FILE} (Autolimitado a 10MB)")
|
|
|
+
|
|
|
+ log(f"=====================================================")
|
|
|
+ log(f"🔥 Servidor Robusto Iniciado en Puerto {LISTENING_PORT}")
|
|
|
+ log(f"🎯 Destino Interno: {SSH_HOST}:{SSH_PORT}")
|
|
|
+ log(f"🛡️ Logs limitados a 10MB en {LOG_FILE}")
|
|
|
+ log(f"=====================================================")
|
|
|
|
|
|
while True:
|
|
|
client, addr = server.accept()
|
|
|
-
|
|
|
with conn_lock:
|
|
|
if active_connections >= MAX_CONNECTIONS:
|
|
|
- log("⛔ Máximo de conexiones alcanzado.", addr)
|
|
|
client.close()
|
|
|
continue
|
|
|
active_connections += 1
|
|
|
-
|
|
|
ConnectionHandler(client, addr).start()
|
|
|
|
|
|
except Exception as e:
|