refactor: improve socket handling, optimize node IP enrichment, and enforce thread safety in VPN management

This commit is contained in:
baoweise-bot
2026-06-05 00:54:16 +08:00
parent 0c2f3a84f0
commit ff9d264f3c
4 changed files with 279 additions and 188 deletions
+11 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)