Automatización de la Gestión de Proxies en Clash | Original, traducido por IA

Home PDF

Esta publicación detalla un script de Python, clash.py, diseñado para automatizar la gestión de tu configuración de proxy Clash. Maneja todo, desde descargar periódicamente configuraciones de proxy actualizadas y reiniciar el servicio Clash hasta seleccionar y cambiar inteligentemente al proxy disponible más rápido dentro de un grupo designado. Complementando a clash.py, el módulo speed.py facilita pruebas de latencia concurrentes de proxies individuales de Clash, asegurando que tu conexión siempre se enrute a través del servidor óptimo.

clash.py

import os
import subprocess
import time
import shutil
import argparse
import logging
import requests
import json
import urllib.parse

# Asumiendo que speed.py está en el mismo directorio o accesible en PYTHONPATH
from speed import get_top_proxies 

# --- Configuración ---
CLASH_CONTROLLER_HOST = "127.0.0.1"
CLASH_CONTROLLER_PORT = 9090
CLASH_API_BASE_URL = f"http://{CLASH_CONTROLLER_HOST}:{CLASH_CONTROLLER_PORT}"
# El nombre del grupo de proxy al que se asignará el mejor proxy individual.
# Asegúrate de que este grupo exista en tu configuración de Clash.
TARGET_PROXY_GROUP = "🚧Proxy" 

def setup_logging():
    """Configura el registro básico para el script."""
    logging.basicConfig(
        filename='clash.log', 
        level=logging.INFO,
        format='%(asctime)s - %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )

def start_system_proxy(global_proxy_address):
    """Establece variables de entorno de proxy a nivel del sistema."""
    os.environ["GLOBAL_PROXY"] = global_proxy_address # Establece para consistencia si se necesita en otro lugar
    os.environ["HTTP_PROXY"] = f"http://{global_proxy_address}"
    os.environ["HTTPS_PROXY"] = f"http://{global_proxy_address}"
    os.environ["http_proxy"] = f"http://{global_proxy_address}"
    os.environ["https_proxy"] = f"http://{global_proxy_address}"
    # Estos normalmente no necesitan ser explícitamente establecidos como "false" con herramientas modernas,
    # pero se mantienen para compatibilidad con la intención original de tu script.
    os.environ["HTTP_PROXY_REQUEST_FULLURI"] = "false" 
    os.environ["HTTPS_PROXY_REQUEST_FULLURI"] = "false"
    os.environ["ALL_PROXY"] = os.environ["http_proxy"]
    logging.info(f"Proxy a nivel del sistema establecido en: {global_proxy_address}")

def stop_system_proxy():
    """Borra las variables de entorno de proxy a nivel del sistema."""
    os.environ["http_proxy"] = ""
    os.environ["HTTP_PROXY"] = ""
    os.environ["https_proxy"] = ""
    os.environ["HTTPS_PROXY"] = ""
    os.environ["HTTP_PROXY_REQUEST_FULLURI"] = "true" # Revertir a predeterminado
    os.environ["HTTPS_PROXY_REQUEST_FULLURI"] = "true"
    os.environ["ALL_PROXY"] = ""
    logging.info("Proxy a nivel del sistema detenido (variables de entorno borradas).")

def switch_clash_proxy_group(group_name, proxy_name):
    """
    Cambia el proxy activo en un grupo de proxy de Clash especificado a un nuevo proxy.
    """
    encoded_group_name = urllib.parse.quote(group_name)
    url = f"{CLASH_API_BASE_URL}/proxies/{encoded_group_name}"
    headers = {"Content-Type": "application/json"}
    payload = {"name": proxy_name}
    
    try:
        response = requests.put(url, headers=headers, data=json.dumps(payload), timeout=5)
        response.raise_for_status()
        logging.info(f"Se cambió exitosamente '{group_name}' a '{proxy_name}'.")
        return True
    except requests.exceptions.ConnectionError:
        logging.error(f"Error: No se pudo conectar a la API de Clash en {CLASH_API_BASE_URL} para cambiar el proxy.")
        logging.error("Asegúrate de que Clash esté ejecutándose y su external-controller esté configurado.")
        return False
    except requests.exceptions.Timeout:
        logging.error(f"Error: La conexión a la API de Clash se agotó mientras se cambiaba el proxy para '{group_name}'.")
        return False
    except requests.exceptions.RequestException as e:
        logging.error(f"Ocurrió un error inesperado al cambiar el proxy para '{group_name}': {e}")
        return False

def main():
    """Función principal para gestionar la configuración de Clash, reiniciar y seleccionar el mejor proxy."""
    setup_logging()
    
    parser = argparse.ArgumentParser(description="Script de configuración y gestión de Clash.")
    parser.add_argument("--minutes", type=int, default=10, help="Minutos entre actualizaciones (predeterminado: 10)")
    parser.add_argument("--iterations", type=int, default=1000, help="Número de iteraciones (predeterminado: 1000)")
    parser.add_argument(
        "--config-url", 
        type=str, 
        default=os.getenv("CLASH_DOWNLOAD_URL"),
        help="URL para descargar la configuración de Clash. Por defecto, usa la variable de entorno CLASH_DOWNLOAD_URL si está configurada, de lo contrario, una URL codificada."
    )
    args = parser.parse_args()

    ITERATIONS = args.iterations
    SLEEP_SECONDS = args.minutes * 60
    config_download_url = args.config_url

    if not config_download_url:
        logging.critical("Error: No se proporcionó URL de descarga de configuración. Por favor, configura la variable de entorno CLASH_DOWNLOAD_URL o usa el argumento --config-url.")
        return # Salir si no hay URL disponible

    clash_executable_path = "/home/lzw/clash-linux-386-v1.17.0/clash-linux-386"
    clash_config_dir = os.path.expanduser("~/.config/clash")
    clash_config_path = os.path.join(clash_config_dir, "config.yaml")

    for i in range(1, ITERATIONS + 1):
        logging.info(f"--- Iniciando Iteración {i} de {ITERATIONS} ---")

        # Paso 1: Detener cualquier configuración de proxy del sistema existente
        stop_system_proxy()
        
        # Paso 2: Descargar y actualizar la configuración de Clash
        try:
            logging.info(f"Descargando nueva configuración desde: {config_download_url}")
            subprocess.run(["wget", config_download_url, "-O", "zhs4.yaml"], check=True, capture_output=True)
            os.makedirs(clash_config_dir, exist_ok=True)
            shutil.move("zhs4.yaml", clash_config_path)
            logging.info("¡Configuración de Clash actualizada exitosamente!")
        except subprocess.CalledProcessError as e:
            logging.error(f"Fallo al descargar o mover el archivo de configuración: {e.stderr.decode().strip()}")
            logging.error("Saltando a la siguiente iteración.")
            time.sleep(10) # Esperar un poco antes de reintentar
            continue
        except Exception as e:
            logging.error(f"Ocurrió un error inesperado durante la actualización de configuración: {e}")
            logging.error("Saltando a la siguiente iteración.")
            time.sleep(10)
            continue

        # Paso 3: Iniciar Clash en segundo plano
        clash_process = None
        try:
            # Es crucial que Clash inicie con el external-controller habilitado y accesible
            # Esto normalmente se configura dentro del config.yaml mismo.
            clash_process = subprocess.Popen([clash_executable_path], 
                                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            logging.info(f"Clash iniciado con PID {clash_process.pid}")
            # Darle a Clash un momento para inicializar completamente y abrir su puerto API
            time.sleep(5) 
        except FileNotFoundError:
            logging.critical(f"Ejecutable de Clash no encontrado en: {clash_executable_path}")
            logging.critical("Por favor, asegúrate de que la ruta sea correcta y Clash esté instalado.")
            return # Error crítico, salir del script
        except Exception as e:
            logging.error(f"Fallo al iniciar Clash: {e}")
            logging.error("Saltando a la siguiente iteración.")
            if clash_process: clash_process.terminate()
            time.sleep(10)
            continue

        # Paso 4: Probar velocidades de proxy y seleccionar el mejor
        best_proxy_name = None
        try:
            logging.info("Probando velocidades de proxy para encontrar el mejor...")
            top_proxies = get_top_proxies(num_results=1) # Obtener solo el mejor proxy
            if top_proxies:
                best_proxy_name = top_proxies[0]['name']
                logging.info(f"Proxy mejor identificado: '{best_proxy_name}' con latencia {top_proxies[0]['latency']}ms")
            else:
                logging.warning("No hubo pruebas de proxy exitosas. No se puede seleccionar un mejor proxy para esta iteración.")
        except Exception as e:
            logging.error(f"Error durante la prueba de velocidad de proxy: {e}")

        # Paso 5: Cambiar el grupo de proxy de Clash al mejor proxy (si se encontró)
        if best_proxy_name:
            # Antes de configurar el proxy del sistema, asegurarse de que Clash esté configurado correctamente.
            # Establecer el proxy del sistema para apuntar al proxy HTTP local de Clash.
            # Clash normalmente ejecuta su proxy HTTP en el puerto 7890 (o similar, verifica tu configuración).
            clash_local_proxy_address = f"{CLASH_CONTROLLER_HOST}:7890" # Ajustar si tu puerto HTTP de Clash es diferente
            start_system_proxy(clash_local_proxy_address)
            
            if not switch_clash_proxy_group(TARGET_PROXY_GROUP, best_proxy_name):
                logging.error(f"Fallo al cambiar el grupo de Clash '{TARGET_PROXY_GROUP}' a '{best_proxy_name}'.")
        else:
            logging.warning("No se encontró mejor proxy, omitiendo el cambio de grupo de proxy y configuración de proxy del sistema para esta iteración.")
            
        # Paso 6: Esperar la duración especificada
        logging.info(f"Esperando {SLEEP_SECONDS / 60} minutos antes de la siguiente iteración...")
        time.sleep(SLEEP_SECONDS)

        # Paso 7: Detener el proceso de Clash
        if clash_process:
            logging.info("Terminando proceso de Clash...")
            clash_process.terminate()
            try:
                clash_process.wait(timeout=10) # Darle a Clash un poco más de tiempo para apagarse correctamente
                logging.info("Clash detenido exitosamente.")
            except subprocess.TimeoutExpired:
                logging.warning("Clash no terminó correctamente, matando proceso.")
                clash_process.kill()
                clash_process.wait() # Asegurarse de que el proceso esté completamente terminado
            except Exception as e:
                logging.error(f"Error mientras se esperaba que Clash se detuviera: {e}")
        
        logging.info(f"--- Iteración {i} completada ---")

    logging.info(f"Completadas {ITERATIONS} iteraciones. Script finalizado.")

if __name__ == "__main__":
    main()

speed.py

```python import requests import json import urllib.parse import time from concurrent.futures import ThreadPoolExecutor, as_completed import logging # Importar el módulo de registro

— Configuración —

CLASH_CONTROLLER_HOST = “127.0.0.1” # Usar 127.0.0.1 ya que el controlador está en la misma máquina CLASH_CONTROLLER_PORT = 9090 CLASH_API_BASE_URL = f”http://{CLASH_CONTROLLER_HOST}:{CLASH_CONTROLLER_PORT}” LATENCY_TEST_URL = “https://github.com” # URL de prueba actualizada LATENCY_TEST_TIMEOUT_MS = 5000 # Milisegundos CONCURRENT_CONNECTIONS = 10 # Número de pruebas concurrentes

Lista de nombres de grupos de proxy conocidos para excluir de las pruebas de velocidad

Estos normalmente no son nodos individuales sino grupos de políticas o proxies especiales.

EXCLUDE_PROXY_GROUPS = [ “DIRECT”, “REJECT”, “GLOBAL”, # Ya excluido por defecto en la API “🇨🇳国内网站或资源”, “🌵其它规则外”, “🎬Netflix等国外流媒体”, “📦ChatGPT”, “📹Youtube”, “📺爱奇艺等国内流媒体”, “🚧Proxy”, # Agregar cualquier otro nombre de grupo que quieras excluir aquí ]

— Configuración de Registro para speed.py —

Configurar el registro para este script específico

Esto asegura que cuando speed.py sea importado y sus funciones sean llamadas,

su salida vaya a speed.log, separada de clash_manager.log.

logging.basicConfig( filename=’clash.log’, level=logging.INFO, format=’%(asctime)s - %(levelname)s - %(message)s’, datefmt=’%Y-%m-%d %H:%M:%S’ )

Opcionalmente, si también quieres ver la salida en la consola, agrega un StreamHandler:

console_handler = logging.StreamHandler()

console_handler.setLevel(logging.INFO)

formatter = logging.Formatter(‘%(asctime)s - %(levelname)s - %(message)s’)

console_handler.setFormatter(formatter)

logging.getLogger().addHandler(console_handler)

— Lógica del Script —

def get_all_proxy_names(): “"”Obtiene todos los nombres de proxy de la API de Clash, excluyendo grupos conocidos.””” try: response = requests.get(f”{CLASH_API_BASE_URL}/proxies”, timeout=5) response.raise_for_status() # Lanzar una excepción para errores HTTP (4xx o 5xx) proxies_data = response.json()

    all_names = proxies_data.get("proxies", {}).keys()
    
    # Filtrar los nombres de grupo
    filtered_names = [name for name in all_names if name not in EXCLUDE_PROXY_GROUPS]
    
    logging.info(f"Se obtuvieron exitosamente {len(filtered_names)} nombres de proxy probables.")
    return filtered_names
except requests.exceptions.ConnectionError:
    logging.error(f"No se pudo conectar a la API de Clash en {CLASH_API_BASE_URL}. Asegúrate de que Clash esté ejecutándose.")
    return []
except requests.exceptions.Timeout:
    logging.error(f"La conexión a la API de Clash se agotó después de 5 segundos.")
    return []
except requests.exceptions.RequestException as e:
    logging.error(f"Ocurrió un error inesperado al obtener los nombres de proxy: {e}")
    return []

def test_proxy_latency(proxy_name): “"”Prueba la latencia de un solo proxy usando la API de Clash. Retorna una tupla (proxy_name, latency) o (proxy_name, None) en caso de fallo. “”” encoded_proxy_name = urllib.parse.quote(proxy_name) url = f”{CLASH_API_BASE_URL}/proxies/{encoded_proxy_name}/delay” params = { “url”: LATENCY_TEST_URL, “timeout”: LATENCY_TEST_TIMEOUT_MS } try: # el timeout de requests está en segundos, convertir milisegundos response = requests.get(url, params=params, timeout=(LATENCY_TEST_TIMEOUT_MS / 1000) + 1) response.raise_for_status() latency_data = response.json() latency = latency_data.get(“delay”) logging.info(f”Proxy: {proxy_name} - Latencia: {latency}ms”) return proxy_name, latency except requests.exceptions.RequestException as e: logging.warning(f”Error probando ‘{proxy_name}’: {e}”) return proxy_name, None

def get_top_proxies(num_results=5): “”” Prueba las velocidades de los proxies de Clash concurrentemente y retorna los N proxies individuales más rápidos.

Retorna:
    list: Una lista de diccionarios, cada uno contiene 'name' y 'latency' para los mejores proxies.
          Retorna una lista vacía si no se encuentran proxies probables o ocurre un error.
"""
logging.info("Iniciando prueba de velocidad de proxies de Clash vía API Externa (concurrentemente)...")
logging.info(f"Usando URL de prueba: {LATENCY_TEST_URL}")
logging.info(f"Ejecutando {CONCURRENT_CONNECTIONS} pruebas a la vez. Esto puede tomar un momento...")

proxy_names_to_test = get_all_proxy_names()
if not proxy_names_to_test:
    logging.warning("No se encontraron proxies probables o ocurrió un error durante la obtención de nombres de proxy.")
    return []

logging.info(f"Se encontraron {len(proxy_names_to_test)} proxies individuales para probar.")

proxy_latencies = {}

with ThreadPoolExecutor(max_workers=CONCURRENT_CONNECTIONS) as executor:
    future_to_proxy = {executor.submit(test_proxy_latency, name): name for name

Back 2025.06.30 Donate