mirror of
https://github.com/baoweise-bot/aimili-vpngate.git
synced 2026-09-05 23:56:55 +08:00
refactor: improve socket handling, optimize node IP enrichment, and enforce thread safety in VPN management
This commit is contained in:
+11
-6
@@ -955,9 +955,14 @@ chmod +x /usr/bin/ml
|
||||
AUTH_FILE="${INSTALL_DIR}/vpngate_data/ui_auth.json"
|
||||
mkdir -p "${INSTALL_DIR}/vpngate_data"
|
||||
|
||||
is_custom="n"
|
||||
if [ ! -f "$AUTH_FILE" ]; then
|
||||
echo -e "\n${YELLOW}检测到是首次安装,是否需要自定义配置网页端参数(端口/安全后缀/登录账号密码)?${PLAIN}"
|
||||
read -p "是否自定义配置?[y/N]: " is_custom
|
||||
if [ -t 0 ]; then
|
||||
echo -e "\n${YELLOW}检测到是首次安装,是否需要自定义配置网页端参数(端口/安全后缀/登录账号密码)?${PLAIN}"
|
||||
read -p "是否自定义配置?[y/N]: " is_custom
|
||||
else
|
||||
echo -e "\n${YELLOW}检测到是非交互式/无TTY环境安装,已自动跳过网页端参数自定义配置,采用默认随机参数部署。${PLAIN}"
|
||||
fi
|
||||
|
||||
# Initialize defaults
|
||||
UI_PORT=8787
|
||||
@@ -1070,13 +1075,13 @@ else
|
||||
fi
|
||||
sysctl -p >/dev/null 2>&1 || true
|
||||
fi
|
||||
# Apply to currently active interfaces dynamically
|
||||
sysctl -w net.ipv4.conf.all.rp_filter=2 >/dev/null 2>&1 || true
|
||||
sysctl -w net.ipv4.conf.default.rp_filter=2 >/dev/null 2>&1 || true
|
||||
# Apply to currently active interfaces dynamically (prefer native proc write for BusyBox/Alpine compatibility)
|
||||
echo "2" > /proc/sys/net/ipv4/conf/all/rp_filter 2>/dev/null || sysctl -w net.ipv4.conf.all.rp_filter=2 >/dev/null 2>&1 || true
|
||||
echo "2" > /proc/sys/net/ipv4/conf/default/rp_filter 2>/dev/null || sysctl -w net.ipv4.conf.default.rp_filter=2 >/dev/null 2>&1 || true
|
||||
if [ -d "/proc/sys/net/ipv4/conf" ]; then
|
||||
for dev_dir in /proc/sys/net/ipv4/conf/*; do
|
||||
dev_name=$(basename "$dev_dir")
|
||||
sysctl -w net.ipv4.conf.${dev_name}.rp_filter=2 >/dev/null 2>&1 || true
|
||||
echo "2" > "/proc/sys/net/ipv4/conf/${dev_name}/rp_filter" 2>/dev/null || sysctl -w net.ipv4.conf.${dev_name}.rp_filter=2 >/dev/null 2>&1 || true
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
+98
-61
@@ -35,24 +35,27 @@ def resolve_dns_over_tun0(host: str, dns_server: str = "8.8.8.8", timeout: float
|
||||
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)
|
||||
sock = None
|
||||
try:
|
||||
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")
|
||||
if len(part_bytes) > 63:
|
||||
return None
|
||||
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)
|
||||
sock.settimeout(timeout)
|
||||
try:
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, b"tun0")
|
||||
@@ -67,37 +70,23 @@ def resolve_dns_over_tun0(host: str, dns_server: str = "8.8.8.8", timeout: float
|
||||
except Exception:
|
||||
return None
|
||||
finally:
|
||||
sock.close()
|
||||
if sock is not None:
|
||||
try:
|
||||
sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if len(resp) < 12:
|
||||
return None
|
||||
if resp[:2] != tx_id:
|
||||
return None
|
||||
try:
|
||||
if len(resp) < 12:
|
||||
return None
|
||||
if resp[:2] != tx_id:
|
||||
return None
|
||||
|
||||
rcode = resp[3] & 0x0F
|
||||
if rcode != 0:
|
||||
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
|
||||
offset = 12
|
||||
while offset < len(resp):
|
||||
length = resp[offset]
|
||||
if length == 0:
|
||||
@@ -108,18 +97,39 @@ def resolve_dns_over_tun0(host: str, dns_server: str = "8.8.8.8", timeout: float
|
||||
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
|
||||
|
||||
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
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
def create_connection(address: tuple[str, int], timeout: float = 20) -> socket.socket:
|
||||
@@ -227,14 +237,35 @@ def http_client(client: socket.socket, first_byte: bytes) -> None:
|
||||
return
|
||||
|
||||
parsed = urllib.parse.urlsplit(target)
|
||||
if not parsed.hostname:
|
||||
hostname = parsed.hostname
|
||||
port = parsed.port
|
||||
scheme = parsed.scheme
|
||||
if not hostname:
|
||||
# Fallback to Host header
|
||||
for line in lines[1:]:
|
||||
if line.lower().startswith("host:"):
|
||||
host_val = line.split(":", 1)[1].strip()
|
||||
if "[" in host_val and "]" in host_val:
|
||||
host_part, _, port_part = host_val.rpartition("]")
|
||||
hostname = host_part.lstrip("[")
|
||||
if port_part.startswith(":"):
|
||||
p_val = port_part.lstrip(":")
|
||||
port = int(p_val) if p_val.isdigit() else None
|
||||
else:
|
||||
port = None
|
||||
else:
|
||||
host_part, _, port_part = host_val.partition(":")
|
||||
hostname = host_part
|
||||
port = int(port_part) if port_part.isdigit() else None
|
||||
break
|
||||
if not hostname:
|
||||
client.sendall(b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n")
|
||||
return
|
||||
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
||||
port = port or (443 if scheme == "https" else 80)
|
||||
path = urllib.parse.urlunsplit(("", "", parsed.path or "/", parsed.query, ""))
|
||||
headers = [line for line in lines[1:] if not line.lower().startswith(("proxy-connection:", "connection:"))]
|
||||
request = f"{method} {path} {version}\r\n" + "\r\n".join(headers) + "\r\nConnection: close\r\n\r\n"
|
||||
upstream = create_connection((parsed.hostname, port), timeout=20)
|
||||
upstream = create_connection((hostname, port), timeout=20)
|
||||
upstream.sendall(request.encode("iso-8859-1") + rest)
|
||||
relay(client, upstream)
|
||||
except Exception as e:
|
||||
@@ -268,6 +299,7 @@ def proxy_client(client: socket.socket, address: tuple[str, int]) -> None:
|
||||
def start_proxy_server(host: str, port: int) -> None:
|
||||
is_ipv6 = ":" in host or host == ""
|
||||
af = socket.AF_INET6 if is_ipv6 else socket.AF_INET
|
||||
server = None
|
||||
try:
|
||||
server = socket.socket(af, socket.SOCK_STREAM)
|
||||
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
@@ -280,7 +312,12 @@ def start_proxy_server(host: str, port: int) -> None:
|
||||
server.listen(256)
|
||||
print(f"HTTP/SOCKS5 proxy listening on {host}:{port}", flush=True)
|
||||
except Exception as e:
|
||||
if is_ipv6 and host == "::":
|
||||
if server is not None:
|
||||
try:
|
||||
server.close()
|
||||
except Exception:
|
||||
pass
|
||||
if is_ipv6 and host in ("::", ""):
|
||||
print(f"[警告] 绑定 IPv6 {host}:{port} 失败 ({e}),正在尝试回退至 IPv4 0.0.0.0 ...", flush=True)
|
||||
try:
|
||||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
||||
+58
-35
@@ -86,6 +86,12 @@ COUNTRY_TRANSLATIONS = {
|
||||
"Luxembourg": "卢森堡",
|
||||
}
|
||||
|
||||
def _safe_int(val: Any, default: int = 0) -> int:
|
||||
try:
|
||||
return int(val)
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
def get_upstream_proxy() -> tuple[str | None, str | None, int | None]:
|
||||
"""
|
||||
Returns (proxy_type, host, port) from environment variables.
|
||||
@@ -100,7 +106,7 @@ def get_upstream_proxy() -> tuple[str | None, str | None, int | None]:
|
||||
else:
|
||||
parts = socks_env.split(":")
|
||||
if len(parts) == 2:
|
||||
return "socks", parts[0], int(parts[1])
|
||||
return "socks", parts[0], _safe_int(parts[1], 10808)
|
||||
elif len(parts) == 1:
|
||||
return "socks", parts[0], 10808
|
||||
|
||||
@@ -113,7 +119,7 @@ def get_upstream_proxy() -> tuple[str | None, str | None, int | None]:
|
||||
else:
|
||||
parts = http_env.split(":")
|
||||
if len(parts) == 2:
|
||||
return "http", parts[0], int(parts[1])
|
||||
return "http", parts[0], _safe_int(parts[1], 10808)
|
||||
elif len(parts) == 1:
|
||||
return "http", parts[0], 10808
|
||||
|
||||
@@ -129,7 +135,7 @@ def get_upstream_proxy() -> tuple[str | None, str | None, int | None]:
|
||||
else:
|
||||
parts = val.split(":")
|
||||
if len(parts) == 2:
|
||||
return "http", parts[0], int(parts[1])
|
||||
return "http", parts[0], _safe_int(parts[1], 10808)
|
||||
return None, None, None
|
||||
|
||||
def is_config_tcp(config_text: str) -> bool:
|
||||
@@ -163,6 +169,8 @@ def parse_remote(config_text: str, fallback_ip: str = "") -> tuple[str, int, str
|
||||
elif parts[0].lower() == "remote" and len(parts) >= 3:
|
||||
remote_host = parts[1]
|
||||
remote_port = int(parts[2]) if parts[2].isdigit() else 0
|
||||
if len(parts) >= 4:
|
||||
proto = parts[3].lower()
|
||||
return remote_host, remote_port, proto
|
||||
|
||||
def get_physical_interface() -> str | None:
|
||||
@@ -171,14 +179,14 @@ def get_physical_interface() -> str | None:
|
||||
if res.returncode == 0:
|
||||
routes = []
|
||||
for line in res.stdout.splitlines():
|
||||
if line.startswith("default via"):
|
||||
if line.startswith("default"):
|
||||
parts = line.split()
|
||||
try:
|
||||
gw = parts[2]
|
||||
dev = parts[parts.index("dev") + 1]
|
||||
metric = 0
|
||||
if "metric" in parts:
|
||||
metric = int(parts[parts.index("metric") + 1])
|
||||
gw = parts[parts.index("via") + 1] if "via" in parts else ""
|
||||
routes.append((gw, dev, metric))
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
@@ -196,8 +204,9 @@ def tcp_latency_ms(host: str, port: int, dev: str | None = None) -> int:
|
||||
started = time.time()
|
||||
# Auto-detect address family based on host address
|
||||
af = socket.AF_INET6 if ":" in host else socket.AF_INET
|
||||
s = socket.socket(af, socket.SOCK_STREAM)
|
||||
s = None
|
||||
try:
|
||||
s = socket.socket(af, socket.SOCK_STREAM)
|
||||
s.settimeout(5)
|
||||
if dev:
|
||||
try:
|
||||
@@ -209,10 +218,11 @@ def tcp_latency_ms(host: str, port: int, dev: str | None = None) -> int:
|
||||
except OSError:
|
||||
return 0
|
||||
finally:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
if s is not None:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def ping_latency_ms(host: str, port: int, fallback_ping: int = 0) -> int:
|
||||
dev = get_physical_interface()
|
||||
@@ -286,8 +296,9 @@ def check_and_fix_dns() -> None:
|
||||
("2606:4700:4700::1111", 53, socket.AF_INET6),
|
||||
]
|
||||
for ip, port, af in dns_targets:
|
||||
s = socket.socket(af, socket.SOCK_DGRAM)
|
||||
s = None
|
||||
try:
|
||||
s = socket.socket(af, socket.SOCK_DGRAM)
|
||||
s.settimeout(2)
|
||||
s.connect((ip, port))
|
||||
network_ok = True
|
||||
@@ -295,10 +306,11 @@ def check_and_fix_dns() -> None:
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
if s is not None:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not network_ok:
|
||||
return
|
||||
@@ -373,7 +385,11 @@ def enrich_ip_info(nodes: list[dict[str, Any]]) -> None:
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=15) as response:
|
||||
data = json.loads(response.read().decode("utf-8", errors="replace"))
|
||||
if not isinstance(data, list):
|
||||
continue
|
||||
for item in data:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if item.get("status") != "success":
|
||||
continue
|
||||
query_ip = item.get("query")
|
||||
@@ -471,18 +487,20 @@ def diagnose_api_failure(api_url: str = "https://www.vpngate.net/api/iphone/") -
|
||||
# 3. 检查 TCP 连接 API 域名
|
||||
api_conn_ok = False
|
||||
api_af, api_ip = api_addr
|
||||
s = socket.socket(api_af, socket.SOCK_STREAM)
|
||||
s.settimeout(4)
|
||||
s = None
|
||||
try:
|
||||
s = socket.socket(api_af, socket.SOCK_STREAM)
|
||||
s.settimeout(4)
|
||||
s.connect((api_ip, port))
|
||||
api_conn_ok = True
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
if s is not None:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not api_conn_ok:
|
||||
ext_conn_ok = False
|
||||
@@ -494,19 +512,21 @@ def diagnose_api_failure(api_url: str = "https://www.vpngate.net/api/iphone/") -
|
||||
("2606:4700:4700::1111", 53, socket.AF_INET6),
|
||||
]
|
||||
for test_ip, test_port, af in ext_targets:
|
||||
s = socket.socket(af, socket.SOCK_STREAM)
|
||||
s.settimeout(3)
|
||||
s = None
|
||||
try:
|
||||
s = socket.socket(af, socket.SOCK_STREAM)
|
||||
s.settimeout(3)
|
||||
s.connect((test_ip, test_port))
|
||||
ext_conn_ok = True
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
if s is not None:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
if ext_conn_ok:
|
||||
return 1008, f"[ERR_API_IP_BLOCKED_OR_DOWN] 连接 API 服务器失败。原因: 外部网络连接通畅,但无法建立到 {domain} ({api_ip}:{port}) 的连接,可能是由于官方 IP 遭 GFW/防火墙 IP 阻断封锁或官方服务器宕机。"
|
||||
else:
|
||||
@@ -549,18 +569,21 @@ def diagnose_local_obstructions(proxy_port: int = 7928, host: str = "127.0.0.1")
|
||||
# 1. 检查端口是否被占用
|
||||
is_ipv6 = ":" in host or host == ""
|
||||
af = socket.AF_INET6 if is_ipv6 else socket.AF_INET
|
||||
s = socket.socket(af, socket.SOCK_STREAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
s = None
|
||||
try:
|
||||
s = socket.socket(af, socket.SOCK_STREAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
s.bind((host, proxy_port))
|
||||
except OSError as e:
|
||||
if e.errno == 98 or e.errno == 10048 or "already in use" in str(e).lower():
|
||||
return 3005, f"[ERR_PORT_IN_USE] 本地代理端口 {proxy_port} 被占用。原因: 其他进程已抢占该端口,导致本系统代理网关启动失败。请运行 'lsof -i :{proxy_port}' 检查占用进程。"
|
||||
if e.errno == 98 or e.errno == 10048 or "already in use" in str(e).lower() or "not supported" in str(e).lower():
|
||||
if e.errno in (98, 10048) or "already in use" in str(e).lower():
|
||||
return 3005, f"[ERR_PORT_IN_USE] 本地代理端口 {proxy_port} 被占用。原因: 其他进程已抢占该端口,导致本系统代理网关启动失败。请运行 'lsof -i :{proxy_port}' 检查占用进程。"
|
||||
finally:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
if s is not None:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
# 2. 检查 IPv4 转发是否开启
|
||||
|
||||
+112
-86
@@ -329,9 +329,12 @@ def fetch_api_text_via_proxy(url: str, ptype: str, phost: str, pport: int, use_s
|
||||
if parsed.query:
|
||||
path += "?" + parsed.query
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(12)
|
||||
is_ipv6 = ":" in phost
|
||||
af = socket.AF_INET6 if is_ipv6 else socket.AF_INET
|
||||
s = None
|
||||
try:
|
||||
s = socket.socket(af, socket.SOCK_STREAM)
|
||||
s.settimeout(12)
|
||||
s.connect((phost, pport))
|
||||
if ptype == "socks":
|
||||
# SOCKS5 Handshake
|
||||
@@ -390,10 +393,11 @@ def fetch_api_text_via_proxy(url: str, ptype: str, phost: str, pport: int, use_s
|
||||
if len(response_data) > 10 * 1024 * 1024: # max 10MB safety guard
|
||||
break
|
||||
finally:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
if s is not None:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Parse HTTP response
|
||||
header_end = response_data.find(b"\r\n\r\n")
|
||||
@@ -830,26 +834,27 @@ def cleanup_policy_routing() -> None:
|
||||
|
||||
def stop_active_openvpn() -> None:
|
||||
global active_openvpn_process, active_openvpn_node_id
|
||||
cleanup_policy_routing()
|
||||
config_to_delete = None
|
||||
if active_openvpn_node_id:
|
||||
nodes = read_json(NODES_FILE, [])
|
||||
node = next((item for item in nodes if item.get("id") == active_openvpn_node_id), None)
|
||||
if node:
|
||||
config_to_delete = node.get("config_file")
|
||||
|
||||
stop_process(active_openvpn_process)
|
||||
active_openvpn_process = None
|
||||
active_openvpn_node_id = ""
|
||||
kill_existing_openvpn_processes()
|
||||
|
||||
if config_to_delete:
|
||||
try:
|
||||
path = Path(config_to_delete)
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
with lock:
|
||||
cleanup_policy_routing()
|
||||
config_to_delete = None
|
||||
if active_openvpn_node_id:
|
||||
nodes = read_json(NODES_FILE, [])
|
||||
node = next((item for item in nodes if item.get("id") == active_openvpn_node_id), None)
|
||||
if node:
|
||||
config_to_delete = node.get("config_file")
|
||||
|
||||
stop_process(active_openvpn_process)
|
||||
active_openvpn_process = None
|
||||
active_openvpn_node_id = ""
|
||||
kill_existing_openvpn_processes()
|
||||
|
||||
if config_to_delete:
|
||||
try:
|
||||
path = Path(config_to_delete)
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def active_openvpn_running() -> bool:
|
||||
return active_openvpn_process is not None and active_openvpn_process.poll() is None
|
||||
@@ -914,12 +919,11 @@ def test_node_by_id(node_id: str) -> dict[str, Any]:
|
||||
ok, message, _ = run_openvpn_until_ready(config_file, keep_alive=False, route_nopull=True, timeout=12, dev=f"tun{idx}")
|
||||
finally:
|
||||
release_test_index(idx)
|
||||
|
||||
try:
|
||||
if temp_path.exists():
|
||||
temp_path.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if temp_path.exists():
|
||||
temp_path.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
temp_node = {
|
||||
"id": node_id,
|
||||
@@ -977,8 +981,20 @@ def test_multiple_nodes(node_ids: list[str]) -> list[dict[str, Any]]:
|
||||
try:
|
||||
CONFIG_DIR.mkdir(exist_ok=True, parents=True)
|
||||
temp_path.write_text(config_text, encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
return {
|
||||
"id": node_id,
|
||||
"latency_ms": 0,
|
||||
"probe_status": "unavailable",
|
||||
"probe_message": f"Failed to write configuration: {e}",
|
||||
"probed_at": time.time(),
|
||||
"owner": "",
|
||||
"asn": "",
|
||||
"as_name": "",
|
||||
"location": "",
|
||||
"ip_type": "",
|
||||
"quality": "",
|
||||
}
|
||||
|
||||
latency = vpn_utils.ping_latency_ms(h, p, fallback_ping)
|
||||
tun_idx = get_free_test_index()
|
||||
@@ -987,15 +1003,17 @@ def test_multiple_nodes(node_ids: list[str]) -> list[dict[str, Any]]:
|
||||
ok, message, _ = run_openvpn_until_ready(config_file, keep_alive=False, route_nopull=True, timeout=12, dev=dev_name)
|
||||
finally:
|
||||
release_test_index(tun_idx)
|
||||
|
||||
try:
|
||||
if temp_path.exists():
|
||||
temp_path.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if temp_path.exists():
|
||||
temp_path.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
temp_node = {
|
||||
"id": node_id,
|
||||
"ip": n_info.get("ip") or h,
|
||||
"remote_host": h,
|
||||
"remote_port": p,
|
||||
"latency_ms": latency,
|
||||
"probe_status": "available" if ok else "unavailable",
|
||||
"probe_message": message,
|
||||
@@ -1007,19 +1025,6 @@ def test_multiple_nodes(node_ids: list[str]) -> list[dict[str, Any]]:
|
||||
"ip_type": "",
|
||||
"quality": "",
|
||||
}
|
||||
if ok:
|
||||
ip_to_enrich = {
|
||||
"ip": n_info.get("ip"),
|
||||
"remote_host": h,
|
||||
"owner": "",
|
||||
"asn": "",
|
||||
"as_name": "",
|
||||
"location": "",
|
||||
"ip_type": "",
|
||||
"quality": "",
|
||||
}
|
||||
vpn_utils.enrich_ip_info([ip_to_enrich])
|
||||
temp_node.update(ip_to_enrich)
|
||||
return temp_node
|
||||
|
||||
updated_nodes_map = {}
|
||||
@@ -1039,6 +1044,14 @@ def test_multiple_nodes(node_ids: list[str]) -> list[dict[str, Any]]:
|
||||
"latency_ms": 0
|
||||
}
|
||||
|
||||
# 批量查询并丰富可用节点的地理及 ISP 信息,防止并发时被定位 API 接口限流
|
||||
successful_nodes = [res for res in updated_nodes_map.values() if res.get("probe_status") == "available"]
|
||||
if successful_nodes:
|
||||
try:
|
||||
vpn_utils.enrich_ip_info(successful_nodes)
|
||||
except Exception as ee:
|
||||
print(f"[test_multiple_nodes] 批量富化 IP 失败: {ee}", flush=True)
|
||||
|
||||
with lock:
|
||||
current_nodes = read_json(NODES_FILE, [])
|
||||
for n in current_nodes:
|
||||
@@ -1182,8 +1195,9 @@ def connect_node(node_id: str) -> str:
|
||||
active_openvpn_node_id = ""
|
||||
raise RuntimeError(message)
|
||||
|
||||
active_openvpn_process = process
|
||||
active_openvpn_node_id = node_id
|
||||
with lock:
|
||||
active_openvpn_process = process
|
||||
active_openvpn_node_id = node_id
|
||||
|
||||
set_state(active_node_latency="配置路由", last_check_message="正在配置策略路由规则与流量转发...")
|
||||
setup_policy_routing("tun0")
|
||||
@@ -4099,9 +4113,10 @@ def check_proxy_health() -> dict[str, Any]:
|
||||
# 1. 检测代理服务端口是否在监听
|
||||
is_ipv6 = ":" in LOCAL_PROXY_HOST
|
||||
af = socket.AF_INET6 if is_ipv6 else socket.AF_INET
|
||||
s = socket.socket(af, socket.SOCK_STREAM)
|
||||
s.settimeout(1.5)
|
||||
s = None
|
||||
try:
|
||||
s = socket.socket(af, socket.SOCK_STREAM)
|
||||
s.settimeout(1.5)
|
||||
connect_host = LOCAL_PROXY_HOST
|
||||
if connect_host in ("::", "0.0.0.0", ""):
|
||||
connect_host = "::1" if is_ipv6 else "127.0.0.1"
|
||||
@@ -4123,10 +4138,11 @@ def check_proxy_health() -> dict[str, Any]:
|
||||
"error": f"代理服务未运行 ({diag_msg})"
|
||||
}
|
||||
finally:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
if s is not None:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. 检测虚拟网卡 tun0 是否存在 (Linux 下)
|
||||
tun_path = Path("/sys/class/net/tun0")
|
||||
@@ -4157,20 +4173,20 @@ def check_proxy_health() -> dict[str, Any]:
|
||||
url,
|
||||
"--max-time", "5"
|
||||
]
|
||||
try:
|
||||
res = subprocess.run(cmd, capture_output=True, text=True, timeout=6)
|
||||
if res.returncode == 0:
|
||||
lines = res.stdout.strip().splitlines()
|
||||
if len(lines) >= 2:
|
||||
ip = lines[0].strip()
|
||||
time_info = lines[1].strip().split()
|
||||
if len(time_info) == 2:
|
||||
total_time_str, http_code = time_info
|
||||
if http_code == "200" and ip:
|
||||
latency_ms = int(float(total_time_str) * 1000)
|
||||
return {"ok": True, "ip": ip, "latency_ms": latency_ms}
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
res = subprocess.run(cmd, capture_output=True, text=True, timeout=6)
|
||||
if res.returncode == 0:
|
||||
lines = res.stdout.strip().splitlines()
|
||||
if len(lines) >= 2:
|
||||
ip = lines[0].strip()
|
||||
time_info = lines[1].strip().split()
|
||||
if len(time_info) == 2:
|
||||
total_time_str, http_code = time_info
|
||||
if http_code == "200" and ip:
|
||||
latency_ms = int(float(total_time_str) * 1000)
|
||||
return {"ok": True, "ip": ip, "latency_ms": latency_ms}
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
try:
|
||||
@@ -4404,9 +4420,10 @@ class Handler(BaseHTTPRequestHandler):
|
||||
proxy_err = ""
|
||||
is_ipv6 = ":" in LOCAL_PROXY_HOST
|
||||
af = socket.AF_INET6 if is_ipv6 else socket.AF_INET
|
||||
s = socket.socket(af, socket.SOCK_STREAM)
|
||||
s.settimeout(0.5)
|
||||
s = None
|
||||
try:
|
||||
s = socket.socket(af, socket.SOCK_STREAM)
|
||||
s.settimeout(0.5)
|
||||
connect_host = LOCAL_PROXY_HOST
|
||||
if connect_host in ("::", "0.0.0.0", ""):
|
||||
connect_host = "::1" if is_ipv6 else "127.0.0.1"
|
||||
@@ -4426,10 +4443,11 @@ class Handler(BaseHTTPRequestHandler):
|
||||
diag = vpn_utils.diagnose_local_obstructions(LOCAL_PROXY_PORT, host=LOCAL_PROXY_HOST)
|
||||
proxy_err = diag[1] if diag else f"本地代理网关无法连通: {e}"
|
||||
finally:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
if s is not None:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
proxy_gateway_status = {
|
||||
"name": "本地代理网关",
|
||||
"status": "running" if proxy_ok else "stopped",
|
||||
@@ -4797,6 +4815,12 @@ class Tee:
|
||||
self.stdout.flush()
|
||||
self.file.flush()
|
||||
|
||||
def isatty(self) -> bool:
|
||||
return self.stdout.isatty()
|
||||
|
||||
def __getattr__(self, attr: str) -> Any:
|
||||
return getattr(self.stdout, attr)
|
||||
|
||||
def main() -> None:
|
||||
ensure_dirs()
|
||||
kill_existing_openvpn_processes()
|
||||
@@ -4830,8 +4854,9 @@ def main() -> None:
|
||||
is_ipv6 = ":" in LOCAL_PROXY_HOST
|
||||
af = socket.AF_INET6 if is_ipv6 else socket.AF_INET
|
||||
for _ in range(30):
|
||||
s = socket.socket(af, socket.SOCK_STREAM)
|
||||
s = None
|
||||
try:
|
||||
s = socket.socket(af, socket.SOCK_STREAM)
|
||||
s.settimeout(0.5)
|
||||
connect_host = LOCAL_PROXY_HOST
|
||||
if connect_host in ("::", "0.0.0.0", ""):
|
||||
@@ -4855,10 +4880,11 @@ def main() -> None:
|
||||
except Exception:
|
||||
time.sleep(0.5)
|
||||
finally:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
if s is not None:
|
||||
try:
|
||||
s.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if gateway_ready:
|
||||
print("[网关] 代理网关已成功启动监听,启动同步与检测脚本...", flush=True)
|
||||
|
||||
Reference in New Issue
Block a user