diff --git a/vpngate_manager.py b/vpngate_manager.py index 3f828b4..035d5b0 100644 --- a/vpngate_manager.py +++ b/vpngate_manager.py @@ -78,8 +78,8 @@ import vpn_utils import proxy_server API_URL = "https://www.vpngate.net/api/iphone/" -FETCH_INTERVAL_SECONDS = int(os.environ.get("FETCH_INTERVAL_SECONDS", "960")) -CHECK_INTERVAL_SECONDS = int(os.environ.get("CHECK_INTERVAL_SECONDS", "960")) +FETCH_INTERVAL_SECONDS = int(os.environ.get("FETCH_INTERVAL_SECONDS", "1260")) +CHECK_INTERVAL_SECONDS = int(os.environ.get("CHECK_INTERVAL_SECONDS", "1260")) TARGET_VALID_NODES = int(os.environ.get("TARGET_VALID_NODES", "3")) MAX_SCAN_ROWS = int(os.environ.get("MAX_SCAN_ROWS", "300")) OPENVPN_TEST_TIMEOUT_SECONDS = int(os.environ.get("OPENVPN_TEST_TIMEOUT_SECONDS", "35")) @@ -171,7 +171,12 @@ def load_ui_config() -> dict[str, Any]: "secret_path": "EJsW2EeBo9lY", "password": "", "host": "::", - "port": 8787 + "port": 8787, + "routing_mode": "auto", + "force_country": "", + "routing_ip_type": "all", + "connection_enabled": True, + "fixed_node_id": "" } updated = False if auth_file.exists(): @@ -179,6 +184,9 @@ def load_ui_config() -> dict[str, Any]: data = json.loads(auth_file.read_text(encoding="utf-8")) for key, val in data.items(): config[key] = val + for key in ["routing_mode", "force_country", "routing_ip_type", "connection_enabled", "fixed_node_id"]: + if key not in data: + updated = True except Exception: pass @@ -292,6 +300,9 @@ def get_state() -> dict[str, Any]: state["proxy_port"] = ui_cfg.get("proxy_port", 7928) state["routing_mode"] = ui_cfg.get("routing_mode", "auto") state["force_country"] = ui_cfg.get("force_country", "") + state["routing_ip_type"] = ui_cfg.get("routing_ip_type", "all") + state["connection_enabled"] = ui_cfg.get("connection_enabled", True) + state["fixed_node_id"] = ui_cfg.get("fixed_node_id", "") return state @@ -846,7 +857,11 @@ def active_openvpn_running() -> bool: def sort_all_nodes(nodes: list[dict[str, Any]]) -> list[dict[str, Any]]: available_nodes = sorted( [n for n in nodes if n.get("probe_status") == "available" or n.get("active")], - key=lambda n: (parse_int(n.get("latency_ms")) or 999999, -parse_int(n.get("score"))) + key=lambda n: ( + 0 if n.get("ip_type") in ("residential", "mobile") else 1, + parse_int(n.get("latency_ms")) or 999999, + -parse_int(n.get("score")) + ) ) untested_nodes = sorted( [n for n in nodes if n.get("probe_status") == "not_checked" and not n.get("active")], @@ -1008,7 +1023,8 @@ def test_multiple_nodes(node_ids: list[str]) -> list[dict[str, Any]]: return temp_node updated_nodes_map = {} - with concurrent.futures.ThreadPoolExecutor(max_workers=max(1, len(to_test))) as executor: + max_workers = min(80, max(1, len(to_test))) + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: futures = {executor.submit(test_worker, (idx, n)): n["id"] for idx, n in enumerate(to_test)} for future in concurrent.futures.as_completed(futures): nid = futures[future] @@ -1040,20 +1056,16 @@ def auto_switch_node(attempt: int = 0) -> None: return ui_cfg = load_ui_config() + connection_enabled = ui_cfg.get("connection_enabled", True) + if not connection_enabled: + print("[自动切换] 连接已禁用,不进行自动切换。", flush=True) + return + routing_mode = ui_cfg.get("routing_mode", "auto") target_country = ui_cfg.get("force_country", "") if routing_mode == "fixed_ip": - print("[自动切换] 当前处于固定 IP 模式,不进行自动切换。", flush=True) - if active_openvpn_node_id: - if not active_openvpn_running(): - print(f"[自动切换] 固定 IP 模式检测到连接已断开,尝试重新连接原节点: {active_openvpn_node_id}", flush=True) - def reconnect_bg(): - try: - connect_node(active_openvpn_node_id) - except Exception as e: - print(f"[自动切换] 重新连接固定节点失败: {e}", flush=True) - threading.Thread(target=reconnect_bg, daemon=True).start() + print("[自动切换] 当前处于固定 IP 模式,不进行自动连接或切换。", flush=True) return # Find the next best available node @@ -1068,6 +1080,13 @@ def auto_switch_node(attempt: int = 0) -> None: if routing_mode == "fixed_region" and target_country: candidates = [n for n in candidates if n.get("country") == target_country] + # Apply routing_ip_type filter + routing_ip_type = ui_cfg.get("routing_ip_type", "all") + if routing_ip_type == "residential": + candidates = [n for n in candidates if n.get("ip_type") in ("residential", "mobile")] + elif routing_ip_type == "hosting": + candidates = [n for n in candidates if n.get("ip_type") == "hosting"] + candidates.sort(key=lambda n: (parse_int(n.get("latency_ms")) or 999999, -parse_int(n.get("score")))) if candidates: @@ -1117,6 +1136,16 @@ def connect_node(node_id: str) -> str: try: log_to_json("INFO", "VPN", f"开始连接节点: {node_id}") + + ui_cfg = load_ui_config() + ui_cfg["connection_enabled"] = True + if ui_cfg.get("routing_mode") == "fixed_ip": + ui_cfg["fixed_node_id"] = node_id + auth_file = DATA_DIR / "ui_auth.json" + with lock: + DATA_DIR.mkdir(exist_ok=True, parents=True) + auth_file.write_text(json.dumps(ui_cfg, ensure_ascii=False, indent=2), encoding="utf-8") + nodes = read_json(NODES_FILE, []) node = next((item for item in nodes if item.get("id") == node_id), None) if not node: @@ -1215,16 +1244,20 @@ def maintain_valid_nodes(force: bool = False) -> str: with lock: stop_active_openvpn() elif not active_openvpn_running(): - has_active_id = False - with lock: - if active_openvpn_node_id: - has_active_id = True - stop_active_openvpn() - if has_active_id: - print("[维护线程] 检测到当前 OpenVPN 进程已意外退出,准备自动切换节点", flush=True) - is_connecting = False - auto_switch_node() - is_connecting = True + ui_cfg = load_ui_config() + routing_mode = ui_cfg.get("routing_mode", "auto") + connection_enabled = ui_cfg.get("connection_enabled", True) + if connection_enabled and routing_mode != "fixed_ip": + has_active_id = False + with lock: + if active_openvpn_node_id: + has_active_id = True + stop_active_openvpn() + if has_active_id: + print("[维护线程] 检测到当前 OpenVPN 进程已意外退出,准备自动切换节点", flush=True) + is_connecting = False + auto_switch_node() + is_connecting = True try: set_state(is_connecting=True, last_check_message="正在拉取最新的免费 VPN 节点列表...") @@ -1273,22 +1306,14 @@ def maintain_valid_nodes(force: bool = False) -> str: write_json(NODES_FILE, merged) - # Test the first 10 non-active nodes from the new list + # Test all non-active nodes from the list with lock: current_nodes = read_json(NODES_FILE, []) - ui_cfg = load_ui_config() - routing_mode = ui_cfg.get("routing_mode", "auto") - target_country = ui_cfg.get("force_country", "") - - if routing_mode == "fixed_region" and target_country: - to_test = [n for n in current_nodes if not n.get("active") and n.get("country") == target_country][:10] - else: - to_test = [n for n in current_nodes if not n.get("active")][:10] - + to_test = [n for n in current_nodes if not n.get("active")] to_test_ids = [n["id"] for n in to_test] - print(f"[维护线程] 正在检测新获取列表的前 10 个节点: {to_test_ids}", flush=True) - set_state(is_connecting=True, last_check_message="正在并发检测筛选可用节点,这可能需要 5-30 秒...") + print(f"[维护线程] 正在并发检测列表中所有节点,共 {len(to_test_ids)} 个...", flush=True) + set_state(is_connecting=True, last_check_message="正在并发检测所有节点可用性...") test_multiple_nodes(to_test_ids) is_connecting = False @@ -1297,19 +1322,25 @@ def maintain_valid_nodes(force: bool = False) -> str: merged = read_json(NODES_FILE, []) if not active_openvpn_running(): ui_cfg = load_ui_config() - routing_mode = ui_cfg.get("routing_mode", "auto") - target_country = ui_cfg.get("force_country", "") - - if routing_mode == "fixed_ip": - if active_openvpn_node_id: - auto_switch_node() - else: - available_candidates = [n for n in merged if n.get("probe_status") == "available"] - if routing_mode == "fixed_region" and target_country: - available_candidates = [n for n in available_candidates if n.get("country") == target_country] + connection_enabled = ui_cfg.get("connection_enabled", True) + if connection_enabled: + routing_mode = ui_cfg.get("routing_mode", "auto") + target_country = ui_cfg.get("force_country", "") - if available_candidates: - auto_switch_node() + if routing_mode != "fixed_ip": + available_candidates = [n for n in merged if n.get("probe_status") == "available"] + if routing_mode == "fixed_region" and target_country: + available_candidates = [n for n in available_candidates if n.get("country") == target_country] + + # Apply routing_ip_type filter for auto-connect + routing_ip_type = ui_cfg.get("routing_ip_type", "all") + if routing_ip_type == "residential": + available_candidates = [n for n in available_candidates if n.get("ip_type") in ("residential", "mobile")] + elif routing_ip_type == "hosting": + available_candidates = [n for n in available_candidates if n.get("ip_type") == "hosting"] + + if available_candidates: + auto_switch_node() valid_nodes_count = len([n for n in merged if n.get("probe_status") == "available"]) message = f"Fetched {len(candidates)} nodes. Tested first 10 nodes." @@ -2507,6 +2538,20 @@ INDEX_HTML = r"""
+
+ + +
+
+ + +
-
+