mirror of
https://github.com/baoweise-bot/aimili-vpngate.git
synced 2026-09-08 09:17:03 +08:00
Improve node fetching and connection failover
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import csv
|
||||
import io
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
MAX_SNAPSHOT_BYTES = 12 * 1024 * 1024
|
||||
MAX_CONFIG_BYTES = 128 * 1024
|
||||
|
||||
REQUIRED_COLUMNS = {
|
||||
"HostName",
|
||||
"IP",
|
||||
"Score",
|
||||
"Ping",
|
||||
"Speed",
|
||||
"CountryLong",
|
||||
"CountryShort",
|
||||
"NumVpnSessions",
|
||||
"OpenVPN_ConfigData_Base64",
|
||||
}
|
||||
|
||||
# VPNGate profiles are data, but OpenVPN profiles can also launch programs.
|
||||
# Keep the accepted surface limited to connection and TLS settings.
|
||||
SAFE_DIRECTIVES = {
|
||||
"auth",
|
||||
"cipher",
|
||||
"client",
|
||||
"comp-lzo",
|
||||
"compress",
|
||||
"connect-retry",
|
||||
"connect-retry-max",
|
||||
"connect-timeout",
|
||||
"data-ciphers",
|
||||
"data-ciphers-fallback",
|
||||
"dev",
|
||||
"dev-type",
|
||||
"dhcp-option",
|
||||
"explicit-exit-notify",
|
||||
"keepalive",
|
||||
"key-direction",
|
||||
"mute",
|
||||
"nobind",
|
||||
"persist-key",
|
||||
"persist-tun",
|
||||
"ping",
|
||||
"ping-restart",
|
||||
"ping-timer-rem",
|
||||
"proto",
|
||||
"pull",
|
||||
"rcvbuf",
|
||||
"remote",
|
||||
"remote-cert-tls",
|
||||
"remote-random",
|
||||
"remote-random-hostname",
|
||||
"reneg-sec",
|
||||
"resolv-retry",
|
||||
"route-delay",
|
||||
"sndbuf",
|
||||
"tls-cipher",
|
||||
"tls-ciphersuites",
|
||||
"tls-client",
|
||||
"tls-version-min",
|
||||
"verb",
|
||||
"verify-x509-name",
|
||||
}
|
||||
SAFE_INLINE_BLOCKS = {"ca", "cert", "key", "tls-auth", "tls-crypt"}
|
||||
|
||||
|
||||
def decode_config(encoded: str) -> str:
|
||||
compact = "".join(str(encoded or "").split())
|
||||
if not compact:
|
||||
raise ValueError("OpenVPN configuration is empty")
|
||||
raw = base64.b64decode(compact.encode("ascii"), validate=True)
|
||||
if not raw or len(raw) > MAX_CONFIG_BYTES:
|
||||
raise ValueError("OpenVPN configuration size is invalid")
|
||||
return raw.decode("utf-8", errors="strict")
|
||||
|
||||
|
||||
def validate_openvpn_config(config_text: str) -> None:
|
||||
if not config_text or len(config_text.encode("utf-8")) > MAX_CONFIG_BYTES:
|
||||
raise ValueError("OpenVPN configuration size is invalid")
|
||||
if "\x00" in config_text:
|
||||
raise ValueError("OpenVPN configuration contains a NUL byte")
|
||||
|
||||
directives: set[str] = set()
|
||||
current_block: str | None = None
|
||||
remote_seen = False
|
||||
for raw_line in config_text.splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith(("#", ";")):
|
||||
continue
|
||||
if line.startswith("<") and line.endswith(">"):
|
||||
tag = line[1:-1].strip().lower()
|
||||
if tag.startswith("/"):
|
||||
if current_block != tag[1:]:
|
||||
raise ValueError(f"Unexpected closing OpenVPN block: {tag}")
|
||||
current_block = None
|
||||
continue
|
||||
if current_block is not None or tag not in SAFE_INLINE_BLOCKS:
|
||||
raise ValueError(f"Unsafe OpenVPN inline block: {tag}")
|
||||
current_block = tag
|
||||
directives.add(f"<{tag}>")
|
||||
continue
|
||||
if current_block is not None:
|
||||
continue
|
||||
|
||||
parts = re.split(r"\s+", line)
|
||||
directive = parts[0].lstrip("-").lower()
|
||||
if directive not in SAFE_DIRECTIVES:
|
||||
raise ValueError(f"Unsafe OpenVPN directive: {directive}")
|
||||
directives.add(directive)
|
||||
|
||||
if directive == "remote":
|
||||
if len(parts) < 3:
|
||||
raise ValueError("OpenVPN remote directive is incomplete")
|
||||
try:
|
||||
port = int(parts[2])
|
||||
except ValueError as exc:
|
||||
raise ValueError("OpenVPN remote port is invalid") from exc
|
||||
if not (1 <= port <= 65535):
|
||||
raise ValueError("OpenVPN remote port is out of range")
|
||||
remote_seen = True
|
||||
elif directive == "dev" and (len(parts) < 2 or not re.fullmatch(r"tun\d*", parts[1].lower())):
|
||||
raise ValueError("Only TUN OpenVPN devices are accepted")
|
||||
|
||||
if current_block is not None:
|
||||
raise ValueError(f"Unclosed OpenVPN inline block: {current_block}")
|
||||
if "client" not in directives or not remote_seen:
|
||||
raise ValueError("OpenVPN client or remote directive is missing")
|
||||
if not {"<ca>", "<cert>", "<key>"}.issubset(directives):
|
||||
raise ValueError("OpenVPN certificate blocks are incomplete")
|
||||
|
||||
|
||||
def parse_and_validate_snapshot(text: str, max_rows: int | None = None) -> list[dict[str, str]]:
|
||||
raw_size = len(text.encode("utf-8"))
|
||||
if raw_size <= 0 or raw_size > MAX_SNAPSHOT_BYTES:
|
||||
raise ValueError("VPNGate snapshot size is invalid")
|
||||
|
||||
lines = [line for line in text.splitlines() if line and not line.startswith("*")]
|
||||
if not lines:
|
||||
raise ValueError("VPNGate snapshot is empty")
|
||||
if lines[0].startswith("#"):
|
||||
lines[0] = lines[0][1:]
|
||||
|
||||
reader = csv.DictReader(io.StringIO("\n".join(lines)))
|
||||
fieldnames = set(reader.fieldnames or [])
|
||||
if not REQUIRED_COLUMNS.issubset(fieldnames):
|
||||
raise ValueError("VPNGate snapshot columns are incomplete")
|
||||
|
||||
rows: list[dict[str, str]] = []
|
||||
for row in reader:
|
||||
normalized = {str(key): str(value or "") for key, value in row.items() if key is not None}
|
||||
if not normalized.get("IP") or not normalized.get("OpenVPN_ConfigData_Base64"):
|
||||
continue
|
||||
try:
|
||||
config_text = decode_config(normalized["OpenVPN_ConfigData_Base64"])
|
||||
validate_openvpn_config(config_text)
|
||||
except (UnicodeError, ValueError):
|
||||
continue
|
||||
rows.append(normalized)
|
||||
if max_rows is not None and len(rows) >= max_rows:
|
||||
break
|
||||
if not rows:
|
||||
raise ValueError("VPNGate snapshot contains no valid nodes")
|
||||
return rows
|
||||
|
||||
|
||||
def snapshot_summary(text: str) -> dict[str, Any]:
|
||||
rows = parse_and_validate_snapshot(text)
|
||||
return {"row_count": len(rows), "byte_count": len(text.encode("utf-8"))}
|
||||
Reference in New Issue
Block a user