|
| 1 | +--- |
| 2 | +audio: false |
| 3 | +generated: false |
| 4 | +lang: es |
| 5 | +layout: post |
| 6 | +title: Automatización de la Gestión de Proxies en Clash |
| 7 | +translated: true |
| 8 | +--- |
| 9 | + |
| 10 | +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. |
| 11 | + |
| 12 | +## clash.py |
| 13 | + |
| 14 | +```python |
| 15 | +import os |
| 16 | +import subprocess |
| 17 | +import time |
| 18 | +import shutil |
| 19 | +import argparse |
| 20 | +import logging |
| 21 | +import requests |
| 22 | +import json |
| 23 | +import urllib.parse |
| 24 | + |
| 25 | +# Asumiendo que speed.py está en el mismo directorio o accesible en PYTHONPATH |
| 26 | +from speed import get_top_proxies |
| 27 | + |
| 28 | +# --- Configuración --- |
| 29 | +CLASH_CONTROLLER_HOST = "127.0.0.1" |
| 30 | +CLASH_CONTROLLER_PORT = 9090 |
| 31 | +CLASH_API_BASE_URL = f"http://{CLASH_CONTROLLER_HOST}:{CLASH_CONTROLLER_PORT}" |
| 32 | +# El nombre del grupo de proxy al que se asignará el mejor proxy individual. |
| 33 | +# Asegúrate de que este grupo exista en tu configuración de Clash. |
| 34 | +TARGET_PROXY_GROUP = "🚧Proxy" |
| 35 | + |
| 36 | +def setup_logging(): |
| 37 | + """Configura el registro básico para el script.""" |
| 38 | + logging.basicConfig( |
| 39 | + filename='clash.log', |
| 40 | + level=logging.INFO, |
| 41 | + format='%(asctime)s - %(message)s', |
| 42 | + datefmt='%Y-%m-%d %H:%M:%S' |
| 43 | + ) |
| 44 | + |
| 45 | +def start_system_proxy(global_proxy_address): |
| 46 | + """Establece variables de entorno de proxy a nivel del sistema.""" |
| 47 | + os.environ["GLOBAL_PROXY"] = global_proxy_address # Establece para consistencia si se necesita en otro lugar |
| 48 | + os.environ["HTTP_PROXY"] = f"http://{global_proxy_address}" |
| 49 | + os.environ["HTTPS_PROXY"] = f"http://{global_proxy_address}" |
| 50 | + os.environ["http_proxy"] = f"http://{global_proxy_address}" |
| 51 | + os.environ["https_proxy"] = f"http://{global_proxy_address}" |
| 52 | + # Estos normalmente no necesitan ser explícitamente establecidos como "false" con herramientas modernas, |
| 53 | + # pero se mantienen para compatibilidad con la intención original de tu script. |
| 54 | + os.environ["HTTP_PROXY_REQUEST_FULLURI"] = "false" |
| 55 | + os.environ["HTTPS_PROXY_REQUEST_FULLURI"] = "false" |
| 56 | + os.environ["ALL_PROXY"] = os.environ["http_proxy"] |
| 57 | + logging.info(f"Proxy a nivel del sistema establecido en: {global_proxy_address}") |
| 58 | + |
| 59 | +def stop_system_proxy(): |
| 60 | + """Borra las variables de entorno de proxy a nivel del sistema.""" |
| 61 | + os.environ["http_proxy"] = "" |
| 62 | + os.environ["HTTP_PROXY"] = "" |
| 63 | + os.environ["https_proxy"] = "" |
| 64 | + os.environ["HTTPS_PROXY"] = "" |
| 65 | + os.environ["HTTP_PROXY_REQUEST_FULLURI"] = "true" # Revertir a predeterminado |
| 66 | + os.environ["HTTPS_PROXY_REQUEST_FULLURI"] = "true" |
| 67 | + os.environ["ALL_PROXY"] = "" |
| 68 | + logging.info("Proxy a nivel del sistema detenido (variables de entorno borradas).") |
| 69 | + |
| 70 | +def switch_clash_proxy_group(group_name, proxy_name): |
| 71 | + """ |
| 72 | + Cambia el proxy activo en un grupo de proxy de Clash especificado a un nuevo proxy. |
| 73 | + """ |
| 74 | + encoded_group_name = urllib.parse.quote(group_name) |
| 75 | + url = f"{CLASH_API_BASE_URL}/proxies/{encoded_group_name}" |
| 76 | + headers = {"Content-Type": "application/json"} |
| 77 | + payload = {"name": proxy_name} |
| 78 | + |
| 79 | + try: |
| 80 | + response = requests.put(url, headers=headers, data=json.dumps(payload), timeout=5) |
| 81 | + response.raise_for_status() |
| 82 | + logging.info(f"Se cambió exitosamente '{group_name}' a '{proxy_name}'.") |
| 83 | + return True |
| 84 | + except requests.exceptions.ConnectionError: |
| 85 | + logging.error(f"Error: No se pudo conectar a la API de Clash en {CLASH_API_BASE_URL} para cambiar el proxy.") |
| 86 | + logging.error("Asegúrate de que Clash esté ejecutándose y su external-controller esté configurado.") |
| 87 | + return False |
| 88 | + except requests.exceptions.Timeout: |
| 89 | + logging.error(f"Error: La conexión a la API de Clash se agotó mientras se cambiaba el proxy para '{group_name}'.") |
| 90 | + return False |
| 91 | + except requests.exceptions.RequestException as e: |
| 92 | + logging.error(f"Ocurrió un error inesperado al cambiar el proxy para '{group_name}': {e}") |
| 93 | + return False |
| 94 | + |
| 95 | +def main(): |
| 96 | + """Función principal para gestionar la configuración de Clash, reiniciar y seleccionar el mejor proxy.""" |
| 97 | + setup_logging() |
| 98 | + |
| 99 | + parser = argparse.ArgumentParser(description="Script de configuración y gestión de Clash.") |
| 100 | + parser.add_argument("--minutes", type=int, default=10, help="Minutos entre actualizaciones (predeterminado: 10)") |
| 101 | + parser.add_argument("--iterations", type=int, default=1000, help="Número de iteraciones (predeterminado: 1000)") |
| 102 | + parser.add_argument( |
| 103 | + "--config-url", |
| 104 | + type=str, |
| 105 | + default=os.getenv("CLASH_DOWNLOAD_URL"), |
| 106 | + 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." |
| 107 | + ) |
| 108 | + args = parser.parse_args() |
| 109 | + |
| 110 | + ITERATIONS = args.iterations |
| 111 | + SLEEP_SECONDS = args.minutes * 60 |
| 112 | + config_download_url = args.config_url |
| 113 | + |
| 114 | + if not config_download_url: |
| 115 | + 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.") |
| 116 | + return # Salir si no hay URL disponible |
| 117 | + |
| 118 | + clash_executable_path = "/home/lzw/clash-linux-386-v1.17.0/clash-linux-386" |
| 119 | + clash_config_dir = os.path.expanduser("~/.config/clash") |
| 120 | + clash_config_path = os.path.join(clash_config_dir, "config.yaml") |
| 121 | + |
| 122 | + for i in range(1, ITERATIONS + 1): |
| 123 | + logging.info(f"--- Iniciando Iteración {i} de {ITERATIONS} ---") |
| 124 | + |
| 125 | + # Paso 1: Detener cualquier configuración de proxy del sistema existente |
| 126 | + stop_system_proxy() |
| 127 | + |
| 128 | + # Paso 2: Descargar y actualizar la configuración de Clash |
| 129 | + try: |
| 130 | + logging.info(f"Descargando nueva configuración desde: {config_download_url}") |
| 131 | + subprocess.run(["wget", config_download_url, "-O", "zhs4.yaml"], check=True, capture_output=True) |
| 132 | + os.makedirs(clash_config_dir, exist_ok=True) |
| 133 | + shutil.move("zhs4.yaml", clash_config_path) |
| 134 | + logging.info("¡Configuración de Clash actualizada exitosamente!") |
| 135 | + except subprocess.CalledProcessError as e: |
| 136 | + logging.error(f"Fallo al descargar o mover el archivo de configuración: {e.stderr.decode().strip()}") |
| 137 | + logging.error("Saltando a la siguiente iteración.") |
| 138 | + time.sleep(10) # Esperar un poco antes de reintentar |
| 139 | + continue |
| 140 | + except Exception as e: |
| 141 | + logging.error(f"Ocurrió un error inesperado durante la actualización de configuración: {e}") |
| 142 | + logging.error("Saltando a la siguiente iteración.") |
| 143 | + time.sleep(10) |
| 144 | + continue |
| 145 | + |
| 146 | + # Paso 3: Iniciar Clash en segundo plano |
| 147 | + clash_process = None |
| 148 | + try: |
| 149 | + # Es crucial que Clash inicie con el external-controller habilitado y accesible |
| 150 | + # Esto normalmente se configura dentro del config.yaml mismo. |
| 151 | + clash_process = subprocess.Popen([clash_executable_path], |
| 152 | + stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| 153 | + logging.info(f"Clash iniciado con PID {clash_process.pid}") |
| 154 | + # Darle a Clash un momento para inicializar completamente y abrir su puerto API |
| 155 | + time.sleep(5) |
| 156 | + except FileNotFoundError: |
| 157 | + logging.critical(f"Ejecutable de Clash no encontrado en: {clash_executable_path}") |
| 158 | + logging.critical("Por favor, asegúrate de que la ruta sea correcta y Clash esté instalado.") |
| 159 | + return # Error crítico, salir del script |
| 160 | + except Exception as e: |
| 161 | + logging.error(f"Fallo al iniciar Clash: {e}") |
| 162 | + logging.error("Saltando a la siguiente iteración.") |
| 163 | + if clash_process: clash_process.terminate() |
| 164 | + time.sleep(10) |
| 165 | + continue |
| 166 | + |
| 167 | + # Paso 4: Probar velocidades de proxy y seleccionar el mejor |
| 168 | + best_proxy_name = None |
| 169 | + try: |
| 170 | + logging.info("Probando velocidades de proxy para encontrar el mejor...") |
| 171 | + top_proxies = get_top_proxies(num_results=1) # Obtener solo el mejor proxy |
| 172 | + if top_proxies: |
| 173 | + best_proxy_name = top_proxies[0]['name'] |
| 174 | + logging.info(f"Proxy mejor identificado: '{best_proxy_name}' con latencia {top_proxies[0]['latency']}ms") |
| 175 | + else: |
| 176 | + logging.warning("No hubo pruebas de proxy exitosas. No se puede seleccionar un mejor proxy para esta iteración.") |
| 177 | + except Exception as e: |
| 178 | + logging.error(f"Error durante la prueba de velocidad de proxy: {e}") |
| 179 | + |
| 180 | + # Paso 5: Cambiar el grupo de proxy de Clash al mejor proxy (si se encontró) |
| 181 | + if best_proxy_name: |
| 182 | + # Antes de configurar el proxy del sistema, asegurarse de que Clash esté configurado correctamente. |
| 183 | + # Establecer el proxy del sistema para apuntar al proxy HTTP local de Clash. |
| 184 | + # Clash normalmente ejecuta su proxy HTTP en el puerto 7890 (o similar, verifica tu configuración). |
| 185 | + clash_local_proxy_address = f"{CLASH_CONTROLLER_HOST}:7890" # Ajustar si tu puerto HTTP de Clash es diferente |
| 186 | + start_system_proxy(clash_local_proxy_address) |
| 187 | + |
| 188 | + if not switch_clash_proxy_group(TARGET_PROXY_GROUP, best_proxy_name): |
| 189 | + logging.error(f"Fallo al cambiar el grupo de Clash '{TARGET_PROXY_GROUP}' a '{best_proxy_name}'.") |
| 190 | + else: |
| 191 | + 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.") |
| 192 | + |
| 193 | + # Paso 6: Esperar la duración especificada |
| 194 | + logging.info(f"Esperando {SLEEP_SECONDS / 60} minutos antes de la siguiente iteración...") |
| 195 | + time.sleep(SLEEP_SECONDS) |
| 196 | + |
| 197 | + # Paso 7: Detener el proceso de Clash |
| 198 | + if clash_process: |
| 199 | + logging.info("Terminando proceso de Clash...") |
| 200 | + clash_process.terminate() |
| 201 | + try: |
| 202 | + clash_process.wait(timeout=10) # Darle a Clash un poco más de tiempo para apagarse correctamente |
| 203 | + logging.info("Clash detenido exitosamente.") |
| 204 | + except subprocess.TimeoutExpired: |
| 205 | + logging.warning("Clash no terminó correctamente, matando proceso.") |
| 206 | + clash_process.kill() |
| 207 | + clash_process.wait() # Asegurarse de que el proceso esté completamente terminado |
| 208 | + except Exception as e: |
| 209 | + logging.error(f"Error mientras se esperaba que Clash se detuviera: {e}") |
| 210 | + |
| 211 | + logging.info(f"--- Iteración {i} completada ---") |
| 212 | + |
| 213 | + logging.info(f"Completadas {ITERATIONS} iteraciones. Script finalizado.") |
| 214 | + |
| 215 | +if __name__ == "__main__": |
| 216 | + main() |
| 217 | +``` |
| 218 | + |
| 219 | +## speed.py |
| 220 | + |
| 221 | +```python |
| 222 | +import requests |
| 223 | +import json |
| 224 | +import urllib.parse |
| 225 | +import time |
| 226 | +from concurrent.futures import ThreadPoolExecutor, as_completed |
| 227 | +import logging # Importar el módulo de registro |
| 228 | + |
| 229 | +# --- Configuración --- |
| 230 | +CLASH_CONTROLLER_HOST = "127.0.0.1" # Usar 127.0.0.1 ya que el controlador está en la misma máquina |
| 231 | +CLASH_CONTROLLER_PORT = 9090 |
| 232 | +CLASH_API_BASE_URL = f"http://{CLASH_CONTROLLER_HOST}:{CLASH_CONTROLLER_PORT}" |
| 233 | +LATENCY_TEST_URL = "https://github.com" # URL de prueba actualizada |
| 234 | +LATENCY_TEST_TIMEOUT_MS = 5000 # Milisegundos |
| 235 | +CONCURRENT_CONNECTIONS = 10 # Número de pruebas concurrentes |
| 236 | + |
| 237 | +# Lista de nombres de grupos de proxy conocidos para excluir de las pruebas de velocidad |
| 238 | +# Estos normalmente no son nodos individuales sino grupos de políticas o proxies especiales. |
| 239 | +EXCLUDE_PROXY_GROUPS = [ |
| 240 | + "DIRECT", |
| 241 | + "REJECT", |
| 242 | + "GLOBAL", # Ya excluido por defecto en la API |
| 243 | + "🇨🇳国内网站或资源", |
| 244 | + "🌵其它规则外", |
| 245 | + "🎬Netflix等国外流媒体", |
| 246 | + "📦ChatGPT", |
| 247 | + "📹Youtube", |
| 248 | + "📺爱奇艺等国内流媒体", |
| 249 | + "🚧Proxy", |
| 250 | + # Agregar cualquier otro nombre de grupo que quieras excluir aquí |
| 251 | +] |
| 252 | + |
| 253 | +# --- Configuración de Registro para speed.py --- |
| 254 | +# Configurar el registro para este script específico |
| 255 | +# Esto asegura que cuando speed.py sea importado y sus funciones sean llamadas, |
| 256 | +# su salida vaya a speed.log, separada de clash_manager.log. |
| 257 | +logging.basicConfig( |
| 258 | + filename='clash.log', |
| 259 | + level=logging.INFO, |
| 260 | + format='%(asctime)s - %(levelname)s - %(message)s', |
| 261 | + datefmt='%Y-%m-%d %H:%M:%S' |
| 262 | +) |
| 263 | +# Opcionalmente, si también quieres ver la salida en la consola, agrega un StreamHandler: |
| 264 | +# console_handler = logging.StreamHandler() |
| 265 | +# console_handler.setLevel(logging.INFO) |
| 266 | +# formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') |
| 267 | +# console_handler.setFormatter(formatter) |
| 268 | +# logging.getLogger().addHandler(console_handler) |
| 269 | + |
| 270 | +# --- Lógica del Script --- |
| 271 | + |
| 272 | +def get_all_proxy_names(): |
| 273 | + """Obtiene todos los nombres de proxy de la API de Clash, excluyendo grupos conocidos.""" |
| 274 | + try: |
| 275 | + response = requests.get(f"{CLASH_API_BASE_URL}/proxies", timeout=5) |
| 276 | + response.raise_for_status() # Lanzar una excepción para errores HTTP (4xx o 5xx) |
| 277 | + proxies_data = response.json() |
| 278 | + |
| 279 | + all_names = proxies_data.get("proxies", {}).keys() |
| 280 | + |
| 281 | + # Filtrar los nombres de grupo |
| 282 | + filtered_names = [name for name in all_names if name not in EXCLUDE_PROXY_GROUPS] |
| 283 | + |
| 284 | + logging.info(f"Se obtuvieron exitosamente {len(filtered_names)} nombres de proxy probables.") |
| 285 | + return filtered_names |
| 286 | + except requests.exceptions.ConnectionError: |
| 287 | + 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.") |
| 288 | + return [] |
| 289 | + except requests.exceptions.Timeout: |
| 290 | + logging.error(f"La conexión a la API de Clash se agotó después de 5 segundos.") |
| 291 | + return [] |
| 292 | + except requests.exceptions.RequestException as e: |
| 293 | + logging.error(f"Ocurrió un error inesperado al obtener los nombres de proxy: {e}") |
| 294 | + return [] |
| 295 | + |
| 296 | +def test_proxy_latency(proxy_name): |
| 297 | + """Prueba la latencia de un solo proxy usando la API de Clash. |
| 298 | + Retorna una tupla (proxy_name, latency) o (proxy_name, None) en caso de fallo. |
| 299 | + """ |
| 300 | + encoded_proxy_name = urllib.parse.quote(proxy_name) |
| 301 | + url = f"{CLASH_API_BASE_URL}/proxies/{encoded_proxy_name}/delay" |
| 302 | + params = { |
| 303 | + "url": LATENCY_TEST_URL, |
| 304 | + "timeout": LATENCY_TEST_TIMEOUT_MS |
| 305 | + } |
| 306 | + try: |
| 307 | + # el timeout de requests está en segundos, convertir milisegundos |
| 308 | + response = requests.get(url, params=params, timeout=(LATENCY_TEST_TIMEOUT_MS / 1000) + 1) |
| 309 | + response.raise_for_status() |
| 310 | + latency_data = response.json() |
| 311 | + latency = latency_data.get("delay") |
| 312 | + logging.info(f"Proxy: {proxy_name} - Latencia: {latency}ms") |
| 313 | + return proxy_name, latency |
| 314 | + except requests.exceptions.RequestException as e: |
| 315 | + logging.warning(f"Error probando '{proxy_name}': {e}") |
| 316 | + return proxy_name, None |
| 317 | + |
| 318 | +def get_top_proxies(num_results=5): |
| 319 | + """ |
| 320 | + Prueba las velocidades de los proxies de Clash concurrentemente y retorna los N proxies individuales más rápidos. |
| 321 | +
|
| 322 | + Retorna: |
| 323 | + list: Una lista de diccionarios, cada uno contiene 'name' y 'latency' para los mejores proxies. |
| 324 | + Retorna una lista vacía si no se encuentran proxies probables o ocurre un error. |
| 325 | + """ |
| 326 | + logging.info("Iniciando prueba de velocidad de proxies de Clash vía API Externa (concurrentemente)...") |
| 327 | + logging.info(f"Usando URL de prueba: {LATENCY_TEST_URL}") |
| 328 | + logging.info(f"Ejecutando {CONCURRENT_CONNECTIONS} pruebas a la vez. Esto puede tomar un momento...") |
| 329 | + |
| 330 | + proxy_names_to_test = get_all_proxy_names() |
| 331 | + if not proxy_names_to_test: |
| 332 | + logging.warning("No se encontraron proxies probables o ocurrió un error durante la obtención de nombres de proxy.") |
| 333 | + return [] |
| 334 | + |
| 335 | + logging.info(f"Se encontraron {len(proxy_names_to_test)} proxies individuales para probar.") |
| 336 | + |
| 337 | + proxy_latencies = {} |
| 338 | + |
| 339 | + with ThreadPoolExecutor(max_workers=CONCURRENT_CONNECTIONS) as executor: |
| 340 | + future_to_proxy = {executor.submit(test_proxy_latency, name): name for name |
0 commit comments