mirror of
https://github.com/baoweise-bot/aimili-vpngate.git
synced 2026-09-08 17:26:46 +08:00
fix: add recursive depth limit to auto_switch_node, improve connection status tracking, and implement UUID-based session management
This commit is contained in:
+98
-78
@@ -306,6 +306,9 @@ def format_line(label, value, target_width=26):
|
|||||||
padding = " " * max(0, target_width - w)
|
padding = " " * max(0, target_width - w)
|
||||||
return f"{prefix}{label}{padding}: {value}"
|
return f"{prefix}{label}{padding}: {value}"
|
||||||
|
|
||||||
|
def print_line(text=""):
|
||||||
|
print(f"{text}\033[K")
|
||||||
|
|
||||||
def print_status():
|
def print_status():
|
||||||
cfg = load_ui_cfg()
|
cfg = load_ui_cfg()
|
||||||
ui_port = cfg.get("port", 8787)
|
ui_port = cfg.get("port", 8787)
|
||||||
@@ -336,36 +339,36 @@ def print_status():
|
|||||||
gateway_status = f"{green}[已激活]{reset}" if gateway_ok else f"{red}[未启动]{reset}"
|
gateway_status = f"{green}[已激活]{reset}" if gateway_ok else f"{red}[未启动]{reset}"
|
||||||
openvpn_status = f"{green}[已连接]{reset}" if openvpn_ok else f"{red}[未连接]{reset}"
|
openvpn_status = f"{green}[已连接]{reset}" if openvpn_ok else f"{red}[未连接]{reset}"
|
||||||
|
|
||||||
print("=======================================================")
|
print_line("=======================================================")
|
||||||
print(f" {bold}AimiliVPN 管理终端 v2.0{reset} ")
|
print_line(f" {bold}AimiliVPN 管理终端 v2.0{reset} ")
|
||||||
print("=======================================================")
|
print_line("=======================================================")
|
||||||
print("【核心服务状态】")
|
print_line("【核心服务状态】")
|
||||||
print(format_line("代理网关 (Port 7928)", gateway_status))
|
print_line(format_line("代理网关 (Port 7928)", gateway_status))
|
||||||
print(format_line(f"管理后台 (Port {ui_port})", backend_status))
|
print_line(format_line(f"管理后台 (Port {ui_port})", backend_status))
|
||||||
print(format_line("连接核心 (OpenVPN)", openvpn_status))
|
print_line(format_line("连接核心 (OpenVPN)", openvpn_status))
|
||||||
|
|
||||||
login_ip = "127.0.0.1" if cfg.get("host") == "127.0.0.1" else get_public_ip()
|
login_ip = "127.0.0.1" if cfg.get("host") == "127.0.0.1" else get_public_ip()
|
||||||
print(format_line("网页登录地址", f"{yellow}http://{login_ip}:{ui_port}/{secret_path}/{reset}"))
|
print_line(format_line("网页登录地址", f"{yellow}http://{login_ip}:{ui_port}/{secret_path}/{reset}"))
|
||||||
print(format_line("网页管理账号", cfg.get("username", "未配置")))
|
print_line(format_line("网页管理账号", cfg.get("username", "未配置")))
|
||||||
curr_pwd = cfg.get("password", "")
|
curr_pwd = cfg.get("password", "")
|
||||||
masked_pwd = curr_pwd if len(curr_pwd) <= 4 else curr_pwd[:3] + "********" + curr_pwd[-2:]
|
masked_pwd = curr_pwd if len(curr_pwd) <= 4 else curr_pwd[:3] + "********" + curr_pwd[-2:]
|
||||||
print(format_line("网页管理密码", masked_pwd))
|
print_line(format_line("网页管理密码", masked_pwd))
|
||||||
print()
|
print_line()
|
||||||
print("【活动节点状态】")
|
print_line("【活动节点状态】")
|
||||||
if is_connecting:
|
if is_connecting:
|
||||||
connecting_msg = state.get('last_check_message') or '正在建立加密隧道并验证路由规则...'
|
connecting_msg = state.get('last_check_message') or '正在建立加密隧道并验证路由规则...'
|
||||||
print(format_line("节点状态", f"{yellow}{connecting_msg}{reset}"))
|
print_line(format_line("节点状态", f"{yellow}{connecting_msg}{reset}"))
|
||||||
elif active_ip:
|
elif active_ip:
|
||||||
print(format_line("节点 IP", active_ip))
|
print_line(format_line("节点 IP", active_ip))
|
||||||
print(format_line("节点地区", active_loc))
|
print_line(format_line("节点地区", active_loc))
|
||||||
print(format_line("节点延迟 (直连测试)", latency))
|
print_line(format_line("节点延迟 (直连测试)", latency))
|
||||||
else:
|
else:
|
||||||
print(format_line("节点状态", "无活动连接"))
|
print_line(format_line("节点状态", "无活动连接"))
|
||||||
print()
|
print_line()
|
||||||
print("【使用方法】")
|
print_line("【使用方法】")
|
||||||
print(f" export http_proxy=socks5://127.0.0.1:7928")
|
print_line(f" export http_proxy=socks5://127.0.0.1:7928")
|
||||||
print(f" export https_proxy=socks5://127.0.0.1:7928")
|
print_line(f" export https_proxy=socks5://127.0.0.1:7928")
|
||||||
print("=======================================================")
|
print_line("=======================================================")
|
||||||
|
|
||||||
def start_service():
|
def start_service():
|
||||||
print("正在启动 AimiliVPN 服务...", flush=True)
|
print("正在启动 AimiliVPN 服务...", flush=True)
|
||||||
@@ -669,18 +672,24 @@ def main():
|
|||||||
elif cmd == "restart":
|
elif cmd == "restart":
|
||||||
restart_service()
|
restart_service()
|
||||||
elif cmd == "status":
|
elif cmd == "status":
|
||||||
|
print("\033[?1049h\033[?25l\033[H\033[J", end="", flush=True)
|
||||||
try:
|
try:
|
||||||
last_state = None
|
last_state = None
|
||||||
while True:
|
while True:
|
||||||
current_state = get_status_state()
|
current_state = get_status_state()
|
||||||
if current_state != last_state:
|
if current_state != last_state:
|
||||||
print("\033[H\033[J", end="")
|
print("\033[H", end="")
|
||||||
print_status()
|
print_status()
|
||||||
print("\n提示: 正在实时监控状态,自动更新。按 Ctrl+C 退出...")
|
print_line("\n\033[1;33m提示: 正在实时监控状态,自动更新。按任意键或 Ctrl+C 退出...\033[0m")
|
||||||
|
print("\033[J", end="", flush=True)
|
||||||
last_state = current_state
|
last_state = current_state
|
||||||
time.sleep(2.0)
|
key = getch_timeout(1.5)
|
||||||
|
if key is not None:
|
||||||
|
break
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
|
finally:
|
||||||
|
print("\033[?1049l\033[?25h", end="", flush=True)
|
||||||
elif cmd == "logs":
|
elif cmd == "logs":
|
||||||
show_logs()
|
show_logs()
|
||||||
elif cmd == "update":
|
elif cmd == "update":
|
||||||
@@ -710,63 +719,74 @@ def main():
|
|||||||
'0': ("退出终端", None)
|
'0': ("退出终端", None)
|
||||||
}
|
}
|
||||||
|
|
||||||
last_state = None
|
# Enter alternate buffer and hide cursor
|
||||||
while True:
|
print("\033[?1049h\033[?25l\033[H\033[J", end="", flush=True)
|
||||||
current_state = get_status_state()
|
try:
|
||||||
if current_state != last_state:
|
|
||||||
print("\033[H\033[J", end="")
|
|
||||||
print_status()
|
|
||||||
|
|
||||||
bold = "\033[1m"
|
|
||||||
reset = "\033[0m"
|
|
||||||
green = "\033[1;32m"
|
|
||||||
|
|
||||||
print(f"【{bold}终端指令菜单栏{reset}】")
|
|
||||||
for key in sorted(options.keys()):
|
|
||||||
if key == '0':
|
|
||||||
continue
|
|
||||||
name, _ = options[key]
|
|
||||||
print(f" {green}[{key}]{reset} {name}")
|
|
||||||
print(f" {green}[0]{reset} {options['0'][0]}")
|
|
||||||
print("=======================================================")
|
|
||||||
print("请直接输入数字键 [0-9] 快速选择执行:", end="", flush=True)
|
|
||||||
last_state = current_state
|
|
||||||
|
|
||||||
try:
|
|
||||||
key = getch()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print()
|
|
||||||
break
|
|
||||||
|
|
||||||
if key == '\x03':
|
|
||||||
print()
|
|
||||||
break
|
|
||||||
|
|
||||||
# '0' 键默认退出终端,并打印换行符以保持 Shell 提示符整洁
|
|
||||||
if key == '0':
|
|
||||||
print()
|
|
||||||
break
|
|
||||||
|
|
||||||
# 回车键 (\r 或 \n) 用于手动刷新当前菜单与连接状态
|
|
||||||
if key in ('\r', '\n', '\x0a', '\x0d'):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Reset last_state to force redraw after any key input
|
|
||||||
last_state = None
|
last_state = None
|
||||||
|
while True:
|
||||||
if key in options:
|
current_state = get_status_state()
|
||||||
name, func = options[key]
|
if current_state != last_state:
|
||||||
if func is None:
|
print("\033[H", end="")
|
||||||
print()
|
print_status()
|
||||||
|
|
||||||
|
bold = "\033[1m"
|
||||||
|
reset = "\033[0m"
|
||||||
|
green = "\033[1;32m"
|
||||||
|
|
||||||
|
print_line(f"【{bold}终端指令菜单栏{reset}】")
|
||||||
|
for key in sorted(options.keys()):
|
||||||
|
if key == '0':
|
||||||
|
continue
|
||||||
|
name, _ = options[key]
|
||||||
|
print_line(f" {green}[{key}]{reset} {name}")
|
||||||
|
print_line(f" {green}[0]{reset} {options['0'][0]}")
|
||||||
|
print_line("=======================================================")
|
||||||
|
print("请直接输入数字键 [0-9] 快速选择执行:\033[K", end="", flush=True)
|
||||||
|
print("\033[J", end="", flush=True)
|
||||||
|
last_state = current_state
|
||||||
|
|
||||||
|
try:
|
||||||
|
key = getch_timeout(1.0)
|
||||||
|
except KeyboardInterrupt:
|
||||||
break
|
break
|
||||||
print("\033[H\033[J", end="")
|
|
||||||
print(f"正在执行: {name}...\n")
|
if key is None:
|
||||||
func()
|
|
||||||
if func in (start_service, stop_service, restart_service):
|
|
||||||
continue
|
continue
|
||||||
if func in (configure_web, configure_port, configure_credentials, show_logs, update_service):
|
|
||||||
|
if key == '\x03' or key == 'q' or key == 'Q':
|
||||||
|
break
|
||||||
|
|
||||||
|
if key == '0':
|
||||||
|
break
|
||||||
|
|
||||||
|
if key in ('\r', '\n', '\x0a', '\x0d'):
|
||||||
|
last_state = None
|
||||||
continue
|
continue
|
||||||
input("\n操作已完成,按回车键返回主菜单...")
|
|
||||||
|
if key in options:
|
||||||
|
name, func = options[key]
|
||||||
|
if func is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Temporarily restore normal terminal scrollback and show cursor
|
||||||
|
print("\033[?1049l\033[?25h", end="", flush=True)
|
||||||
|
print(f"正在执行: {name}...\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
func()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"执行出错: {e}")
|
||||||
|
|
||||||
|
if func not in (start_service, stop_service, restart_service,
|
||||||
|
configure_web, configure_port, configure_credentials, show_logs, update_service):
|
||||||
|
input("\n操作已完成,按回车键返回主菜单...")
|
||||||
|
|
||||||
|
# Re-enter alternate buffer and hide cursor
|
||||||
|
print("\033[?1049h\033[?25l\033[H\033[J", end="", flush=True)
|
||||||
|
last_state = None
|
||||||
|
finally:
|
||||||
|
# Exit alternate buffer and show cursor on exit
|
||||||
|
print("\033[?1049l\033[?25h", end="", flush=True)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
+103
-1
@@ -22,8 +22,103 @@ def recv_exact(sock: socket.socket, size: int) -> bytes:
|
|||||||
data += chunk
|
data += chunk
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def resolve_dns_over_tun0(host: str, dns_server: str = "8.8.8.8", timeout: float = 3.0) -> str | None:
|
||||||
|
try:
|
||||||
|
socket.inet_aton(host)
|
||||||
|
return host
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
import random
|
||||||
|
tx_id = random.getrandbits(16).to_bytes(2, "big")
|
||||||
|
flags = b"\x01\x00"
|
||||||
|
questions = b"\x00\x01"
|
||||||
|
rrs = b"\x00\x00\x00\x00\x00\x00"
|
||||||
|
|
||||||
|
qname = b""
|
||||||
|
for part in host.split("."):
|
||||||
|
if not part:
|
||||||
|
continue
|
||||||
|
part_bytes = part.encode("idna")
|
||||||
|
qname += len(part_bytes).to_bytes(1, "big") + part_bytes
|
||||||
|
qname += b"\x00"
|
||||||
|
|
||||||
|
qtype_qclass = b"\x00\x01\x00\x01"
|
||||||
|
packet = tx_id + flags + questions + rrs + qname + qtype_qclass
|
||||||
|
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
try:
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
try:
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, b"tun0")
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
sock.sendto(packet, (dns_server, 53))
|
||||||
|
resp, _ = sock.recvfrom(2048)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
if len(resp) < 12:
|
||||||
|
return None
|
||||||
|
if resp[:2] != tx_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
rcode = resp[3] & 0x0F
|
||||||
|
if rcode != 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
offset = 12
|
||||||
|
while offset < len(resp):
|
||||||
|
length = resp[offset]
|
||||||
|
if length == 0:
|
||||||
|
offset += 1
|
||||||
|
break
|
||||||
|
elif (length & 0xC0) == 0xC0:
|
||||||
|
offset += 2
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
offset += 1 + length
|
||||||
|
|
||||||
|
offset += 4
|
||||||
|
answers_count = int.from_bytes(resp[6:8], "big")
|
||||||
|
if answers_count == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for _ in range(answers_count):
|
||||||
|
if offset >= len(resp):
|
||||||
|
break
|
||||||
|
while offset < len(resp):
|
||||||
|
length = resp[offset]
|
||||||
|
if length == 0:
|
||||||
|
offset += 1
|
||||||
|
break
|
||||||
|
elif (length & 0xC0) == 0xC0:
|
||||||
|
offset += 2
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
offset += 1 + length
|
||||||
|
if offset + 10 > len(resp):
|
||||||
|
break
|
||||||
|
atype = int.from_bytes(resp[offset : offset + 2], "big")
|
||||||
|
aclass = int.from_bytes(resp[offset + 2 : offset + 4], "big")
|
||||||
|
rdlength = int.from_bytes(resp[offset + 8 : offset + 10], "big")
|
||||||
|
offset += 10
|
||||||
|
if offset + rdlength > len(resp):
|
||||||
|
break
|
||||||
|
if atype == 1 and aclass == 1 and rdlength == 4:
|
||||||
|
ip_bytes = resp[offset : offset + 4]
|
||||||
|
return socket.inet_ntoa(ip_bytes)
|
||||||
|
offset += rdlength
|
||||||
|
return None
|
||||||
|
|
||||||
def create_connection(address: tuple[str, int], timeout: float = 20) -> socket.socket:
|
def create_connection(address: tuple[str, int], timeout: float = 20) -> socket.socket:
|
||||||
host, port = address
|
host, port = address
|
||||||
|
resolved_ip = resolve_dns_over_tun0(host)
|
||||||
|
if resolved_ip:
|
||||||
|
host = resolved_ip
|
||||||
|
|
||||||
err = None
|
err = None
|
||||||
for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
|
for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
|
||||||
af, socktype, proto, canonname, sa = res
|
af, socktype, proto, canonname, sa = res
|
||||||
@@ -76,7 +171,14 @@ def socks5_client(client: socket.socket, first_byte: bytes) -> None:
|
|||||||
client.sendall(b"\x05\x08\x00\x01\x00\x00\x00\x00\x00\x00")
|
client.sendall(b"\x05\x08\x00\x01\x00\x00\x00\x00\x00\x00")
|
||||||
return
|
return
|
||||||
port = int.from_bytes(recv_exact(client, 2), "big")
|
port = int.from_bytes(recv_exact(client, 2), "big")
|
||||||
upstream = create_connection((host, port), timeout=20)
|
try:
|
||||||
|
upstream = create_connection((host, port), timeout=20)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
client.sendall(b"\x05\x04\x00\x01\x00\x00\x00\x00\x00\x00")
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
client.sendall(b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00")
|
client.sendall(b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00")
|
||||||
relay(client, upstream)
|
relay(client, upstream)
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
+140
-90
@@ -21,6 +21,7 @@ from pathlib import Path
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import sys
|
import sys
|
||||||
|
import uuid
|
||||||
|
|
||||||
# Force socket to resolve IPv4 only to avoid slow AAAA (IPv6) DNS resolution timeouts (e.g. in WSL)
|
# Force socket to resolve IPv4 only to avoid slow AAAA (IPv6) DNS resolution timeouts (e.g. in WSL)
|
||||||
_orig_getaddrinfo = socket.getaddrinfo
|
_orig_getaddrinfo = socket.getaddrinfo
|
||||||
@@ -56,9 +57,10 @@ STATE_FILE = DATA_DIR / "state.json"
|
|||||||
AUTH_FILE = DATA_DIR / "vpngate_auth.txt"
|
AUTH_FILE = DATA_DIR / "vpngate_auth.txt"
|
||||||
|
|
||||||
lock = threading.RLock()
|
lock = threading.RLock()
|
||||||
|
active_sessions: dict[str, float] = {}
|
||||||
active_openvpn_process: subprocess.Popen[str] | None = None
|
active_openvpn_process: subprocess.Popen[str] | None = None
|
||||||
active_openvpn_node_id = ""
|
active_openvpn_node_id = ""
|
||||||
is_connecting = False
|
is_connecting = True
|
||||||
last_active_ping_time = 0.0
|
last_active_ping_time = 0.0
|
||||||
last_active_latency = 0
|
last_active_latency = 0
|
||||||
|
|
||||||
@@ -762,7 +764,11 @@ def test_multiple_nodes(node_ids: list[str]) -> list[dict[str, Any]]:
|
|||||||
|
|
||||||
return list(updated_nodes_map.values())
|
return list(updated_nodes_map.values())
|
||||||
|
|
||||||
def auto_switch_node() -> None:
|
def auto_switch_node(attempt: int = 0) -> None:
|
||||||
|
if attempt >= 3:
|
||||||
|
print("[自动切换] 连续切换失败已达 3 次,停止切换以防止主线程死锁,将在后台重新加载节点...", flush=True)
|
||||||
|
return
|
||||||
|
|
||||||
# Find the next best available node
|
# Find the next best available node
|
||||||
with lock:
|
with lock:
|
||||||
nodes = read_json(NODES_FILE, [])
|
nodes = read_json(NODES_FILE, [])
|
||||||
@@ -784,7 +790,7 @@ def auto_switch_node() -> None:
|
|||||||
err_msg = f"切换到备用节点 {next_node['id']} 失败: {e},将尝试下一个..."
|
err_msg = f"切换到备用节点 {next_node['id']} 失败: {e},将尝试下一个..."
|
||||||
print(f"[自动切换] {err_msg}", flush=True)
|
print(f"[自动切换] {err_msg}", flush=True)
|
||||||
log_to_json("WARNING", "VPN", err_msg)
|
log_to_json("WARNING", "VPN", err_msg)
|
||||||
auto_switch_node()
|
auto_switch_node(attempt + 1)
|
||||||
else:
|
else:
|
||||||
msg = "没有可用的备选节点,将自动断开并清理当前连接状态,同时在后台异步获取新节点..."
|
msg = "没有可用的备选节点,将自动断开并清理当前连接状态,同时在后台异步获取新节点..."
|
||||||
print(f"[自动切换] {msg}", flush=True)
|
print(f"[自动切换] {msg}", flush=True)
|
||||||
@@ -885,99 +891,121 @@ def connect_node(node_id: str) -> str:
|
|||||||
is_connecting = False
|
is_connecting = False
|
||||||
|
|
||||||
def maintain_valid_nodes(force: bool = False) -> str:
|
def maintain_valid_nodes(force: bool = False) -> str:
|
||||||
global active_openvpn_process, active_openvpn_node_id
|
global active_openvpn_process, active_openvpn_node_id, is_connecting
|
||||||
ensure_dirs()
|
ensure_dirs()
|
||||||
if force:
|
is_connecting = True
|
||||||
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)
|
|
||||||
auto_switch_node()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
candidates = fetch_candidates()
|
if force:
|
||||||
except Exception as exc:
|
with lock:
|
||||||
vpn_utils.check_and_fix_dns()
|
stop_active_openvpn()
|
||||||
try:
|
elif not active_openvpn_running():
|
||||||
candidates = fetch_candidates()
|
has_active_id = False
|
||||||
except Exception as exc2:
|
with lock:
|
||||||
set_state(last_fetch_at=time.time(), last_fetch_status="error", last_fetch_message=str(exc2))
|
if active_openvpn_node_id:
|
||||||
candidates = []
|
has_active_id = True
|
||||||
|
stop_active_openvpn()
|
||||||
if not candidates:
|
if has_active_id:
|
||||||
return "没有拉取到新节点"
|
print("[维护线程] 检测到当前 OpenVPN 进程已意外退出,准备自动切换节点", flush=True)
|
||||||
|
is_connecting = False
|
||||||
with lock:
|
|
||||||
active_node = None
|
|
||||||
if active_openvpn_node_id:
|
|
||||||
current_nodes = read_json(NODES_FILE, [])
|
|
||||||
active_node = next((n for n in current_nodes if n.get("id") == active_openvpn_node_id), None)
|
|
||||||
|
|
||||||
merged: list[dict[str, Any]] = []
|
|
||||||
seen_ids: set[str] = set()
|
|
||||||
|
|
||||||
if active_node:
|
|
||||||
merged.append(active_node)
|
|
||||||
seen_ids.add(active_node["id"])
|
|
||||||
|
|
||||||
for cand in candidates:
|
|
||||||
if cand["id"] not in seen_ids:
|
|
||||||
merged.append(cand)
|
|
||||||
seen_ids.add(cand["id"])
|
|
||||||
|
|
||||||
if len(merged) > 1000:
|
|
||||||
merged = merged[:1000]
|
|
||||||
|
|
||||||
for n in merged:
|
|
||||||
config_path = Path(n["config_file"])
|
|
||||||
if not config_path.exists():
|
|
||||||
try:
|
|
||||||
config_path.write_text(n["config_text"], encoding="utf-8")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
write_json(NODES_FILE, merged)
|
|
||||||
|
|
||||||
# Test the first 10 non-active nodes from the new list
|
|
||||||
with lock:
|
|
||||||
current_nodes = read_json(NODES_FILE, [])
|
|
||||||
to_test = [n for n in current_nodes if not n.get("active")][:10]
|
|
||||||
to_test_ids = [n["id"] for n in to_test]
|
|
||||||
|
|
||||||
print(f"[维护线程] 正在检测新获取列表的前 10 个节点: {to_test_ids}", flush=True)
|
|
||||||
test_multiple_nodes(to_test_ids)
|
|
||||||
|
|
||||||
with lock:
|
|
||||||
merged = read_json(NODES_FILE, [])
|
|
||||||
if not active_openvpn_running():
|
|
||||||
available_candidates = [n for n in merged if n.get("probe_status") == "available"]
|
|
||||||
if available_candidates:
|
|
||||||
auto_switch_node()
|
auto_switch_node()
|
||||||
|
is_connecting = True
|
||||||
|
|
||||||
valid_nodes_count = len([n for n in merged if n.get("probe_status") == "available"])
|
try:
|
||||||
message = f"Fetched {len(candidates)} nodes. Tested first 10 nodes."
|
set_state(is_connecting=True, last_check_message="正在拉取最新的免费 VPN 节点列表...")
|
||||||
set_state(
|
candidates = fetch_candidates()
|
||||||
last_check_at=time.time(),
|
except Exception as exc:
|
||||||
last_check_message=message,
|
vpn_utils.check_and_fix_dns()
|
||||||
active_openvpn_node_id=active_openvpn_node_id,
|
try:
|
||||||
valid_nodes=valid_nodes_count,
|
set_state(is_connecting=True, last_check_message="解析失败重试:正在拉取最新的免费 VPN 节点列表...")
|
||||||
)
|
candidates = fetch_candidates()
|
||||||
return message
|
except Exception as exc2:
|
||||||
|
set_state(last_fetch_at=time.time(), last_fetch_status="error", last_fetch_message=str(exc2))
|
||||||
|
candidates = []
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
is_connecting = False
|
||||||
|
return "没有拉取到新节点"
|
||||||
|
|
||||||
|
with lock:
|
||||||
|
active_node = None
|
||||||
|
if active_openvpn_node_id:
|
||||||
|
current_nodes = read_json(NODES_FILE, [])
|
||||||
|
active_node = next((n for n in current_nodes if n.get("id") == active_openvpn_node_id), None)
|
||||||
|
|
||||||
|
merged: list[dict[str, Any]] = []
|
||||||
|
seen_ids: set[str] = set()
|
||||||
|
|
||||||
|
if active_node:
|
||||||
|
merged.append(active_node)
|
||||||
|
seen_ids.add(active_node["id"])
|
||||||
|
|
||||||
|
for cand in candidates:
|
||||||
|
if cand["id"] not in seen_ids:
|
||||||
|
merged.append(cand)
|
||||||
|
seen_ids.add(cand["id"])
|
||||||
|
|
||||||
|
if len(merged) > 1000:
|
||||||
|
merged = merged[:1000]
|
||||||
|
|
||||||
|
for n in merged:
|
||||||
|
config_path = Path(n["config_file"])
|
||||||
|
if not config_path.exists():
|
||||||
|
try:
|
||||||
|
config_path.write_text(n["config_text"], encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
write_json(NODES_FILE, merged)
|
||||||
|
|
||||||
|
# Test the first 10 non-active nodes from the new list
|
||||||
|
with lock:
|
||||||
|
current_nodes = read_json(NODES_FILE, [])
|
||||||
|
to_test = [n for n in current_nodes if not n.get("active")][:10]
|
||||||
|
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 秒...")
|
||||||
|
test_multiple_nodes(to_test_ids)
|
||||||
|
|
||||||
|
is_connecting = False
|
||||||
|
|
||||||
|
with lock:
|
||||||
|
merged = read_json(NODES_FILE, [])
|
||||||
|
if not active_openvpn_running():
|
||||||
|
available_candidates = [n for n in merged if n.get("probe_status") == "available"]
|
||||||
|
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."
|
||||||
|
set_state(
|
||||||
|
last_check_at=time.time(),
|
||||||
|
last_check_message=message,
|
||||||
|
active_openvpn_node_id=active_openvpn_node_id,
|
||||||
|
valid_nodes=valid_nodes_count,
|
||||||
|
)
|
||||||
|
return message
|
||||||
|
except Exception as e:
|
||||||
|
is_connecting = False
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
def collector_loop() -> None:
|
def collector_loop() -> None:
|
||||||
while True:
|
while True:
|
||||||
|
success = False
|
||||||
try:
|
try:
|
||||||
maintain_valid_nodes(force=False)
|
res = maintain_valid_nodes(force=False)
|
||||||
|
if "没有拉取到新节点" not in res:
|
||||||
|
success = True
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
set_state(last_check_at=time.time(), last_check_message=f"check error: {exc}")
|
set_state(last_check_at=time.time(), last_check_message=f"check error: {exc}")
|
||||||
time.sleep(CHECK_INTERVAL_SECONDS)
|
|
||||||
|
if not active_openvpn_running() and not success:
|
||||||
|
sleep_time = 30
|
||||||
|
else:
|
||||||
|
sleep_time = CHECK_INTERVAL_SECONDS
|
||||||
|
|
||||||
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
LOGIN_HTML = r"""<!DOCTYPE html>
|
LOGIN_HTML = r"""<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
@@ -3177,7 +3205,6 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
def is_authorized(self) -> bool:
|
def is_authorized(self) -> bool:
|
||||||
ui_cfg = load_ui_config()
|
ui_cfg = load_ui_config()
|
||||||
pwd = ui_cfg.get("password")
|
pwd = ui_cfg.get("password")
|
||||||
uname = ui_cfg.get("username", "admin")
|
|
||||||
if not pwd:
|
if not pwd:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -3190,8 +3217,15 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
k, v = item.split("=", 1)
|
k, v = item.split("=", 1)
|
||||||
cookies[k.strip()] = v.strip()
|
cookies[k.strip()] = v.strip()
|
||||||
|
|
||||||
expected_token = get_session_token(pwd, uname)
|
session_token = cookies.get("session")
|
||||||
return cookies.get("session") == expected_token
|
if not session_token:
|
||||||
|
return False
|
||||||
|
|
||||||
|
with lock:
|
||||||
|
exp_time = active_sessions.get(session_token)
|
||||||
|
if exp_time is not None and exp_time > time.time():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def validate_path(self) -> str:
|
def validate_path(self) -> str:
|
||||||
secret_path = self.get_secret_path()
|
secret_path = self.get_secret_path()
|
||||||
@@ -3299,7 +3333,9 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
expected_uname = ui_cfg.get("username", "admin")
|
expected_uname = ui_cfg.get("username", "admin")
|
||||||
|
|
||||||
if expected_pwd and input_pwd == expected_pwd and input_uname == expected_uname:
|
if expected_pwd and input_pwd == expected_pwd and input_uname == expected_uname:
|
||||||
token = get_session_token(expected_pwd, expected_uname)
|
token = uuid.uuid4().hex
|
||||||
|
with lock:
|
||||||
|
active_sessions[token] = time.time() + 30 * 24 * 3600
|
||||||
self.send_response(HTTPStatus.OK)
|
self.send_response(HTTPStatus.OK)
|
||||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||||
secret_path = self.get_secret_path()
|
secret_path = self.get_secret_path()
|
||||||
@@ -3315,6 +3351,18 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
if effective_path == "/api/logout":
|
if effective_path == "/api/logout":
|
||||||
try:
|
try:
|
||||||
|
cookie_header = self.headers.get("Cookie", "")
|
||||||
|
cookies = {}
|
||||||
|
if cookie_header:
|
||||||
|
for item in cookie_header.split(";"):
|
||||||
|
item = item.strip()
|
||||||
|
if "=" in item:
|
||||||
|
k, v = item.split("=", 1)
|
||||||
|
cookies[k.strip()] = v.strip()
|
||||||
|
session_token = cookies.get("session")
|
||||||
|
if session_token:
|
||||||
|
with lock:
|
||||||
|
active_sessions.pop(session_token, None)
|
||||||
secret_path = self.get_secret_path()
|
secret_path = self.get_secret_path()
|
||||||
cookie_path = f"/{secret_path}/" if secret_path else "/"
|
cookie_path = f"/{secret_path}/" if secret_path else "/"
|
||||||
self.send_response(HTTPStatus.OK)
|
self.send_response(HTTPStatus.OK)
|
||||||
@@ -3502,7 +3550,9 @@ def main() -> None:
|
|||||||
"local_proxy": f"http://{LOCAL_PROXY_HOST}:{LOCAL_PROXY_PORT}",
|
"local_proxy": f"http://{LOCAL_PROXY_HOST}:{LOCAL_PROXY_PORT}",
|
||||||
"active_openvpn_node_id": "",
|
"active_openvpn_node_id": "",
|
||||||
"last_fetch_status": "starting",
|
"last_fetch_status": "starting",
|
||||||
"last_check_message": "service starting",
|
"last_check_message": "服务已启动,正在初始化网络并获取候选 VPN 节点...",
|
||||||
|
"is_connecting": True,
|
||||||
|
"active_node_latency": "正在准备",
|
||||||
"blacklisted_nodes": 0,
|
"blacklisted_nodes": 0,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user