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