From c9895133cb45d3e1fc96b74b61307b99a5ec312e Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 16 Jun 2026 11:56:22 +0800 Subject: [PATCH] fix: run VLESS Reality on Xray instead of sing-box for v2rayN sing-box Hy2 stays on 8443+; port 443 VLESS uses Xray which pairs reliably with v2rayN/Xray-core clients. Co-authored-by: Cursor --- panel/app.py | 32 ++++++-- panel/stats.py | 25 ++++++ scripts/generate-keys.sh | 6 +- scripts/install.sh | 15 +++- scripts/migrate-xray-reality.sh | 30 ++++++++ scripts/render-server.py | 26 +------ scripts/render-xray.py | 132 ++++++++++++++++++++++++++++++++ scripts/uninstall.sh | 7 +- scripts/verify-reality.sh | 71 +++++++++++++++++ 9 files changed, 305 insertions(+), 39 deletions(-) create mode 100644 scripts/migrate-xray-reality.sh create mode 100644 scripts/render-xray.py create mode 100644 scripts/verify-reality.sh diff --git a/panel/app.py b/panel/app.py index d940fd9..948d11e 100644 --- a/panel/app.py +++ b/panel/app.py @@ -27,6 +27,7 @@ from stats import collect_node_stats ROOT = Path(os.environ.get("JIEDIAN_ROOT", Path(__file__).resolve().parents[1])) SECRET_FILE = ROOT / "data" / ".panel_secret" RENDER_SCRIPT = ROOT / "scripts" / "render-server.py" +RENDER_XRAY_SCRIPT = ROOT / "scripts" / "render-xray.py" _apply_lock = threading.Lock() @@ -109,14 +110,28 @@ def render_singbox_config() -> tuple[bool, str]: env=env, ) if proc.returncode != 0: - return False, proc.stderr or proc.stdout or "配置生成失败" + return False, proc.stderr or proc.stdout or "sing-box 配置生成失败" return True, "ok" -def restart_singbox_async() -> None: - """后台重启 sing-box,避免添加/删除节点 API 长时间阻塞。""" +def render_xray_config() -> tuple[bool, str]: + env = os.environ.copy() + env["JIEDIAN_ROOT"] = str(ROOT) + proc = subprocess.run( + ["python3", str(RENDER_XRAY_SCRIPT)], + capture_output=True, + text=True, + env=env, + ) + if proc.returncode != 0: + return False, proc.stderr or proc.stdout or "Xray 配置生成失败" + return True, "ok" + + +def restart_services_async() -> None: + """后台重启 sing-box 与 Xray。""" subprocess.Popen( - ["systemctl", "restart", "sing-box"], + ["systemctl", "restart", "xray", "sing-box"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) @@ -126,10 +141,17 @@ def apply_singbox() -> tuple[bool, str]: ok, msg = render_singbox_config() if not ok: return False, msg - restart_singbox_async() + ok, msg = render_xray_config() + if not ok: + return False, msg + restart_services_async() return True, "ok" +def restart_singbox_async() -> None: + restart_services_async() + + def apply_singbox_background(on_fail=None) -> None: """后台生成配置并重启 sing-box,避免阻塞 HTTP 请求导致 Nginx 503。""" diff --git a/panel/stats.py b/panel/stats.py index 34ad434..d724b7e 100644 --- a/panel/stats.py +++ b/panel/stats.py @@ -19,10 +19,14 @@ ENV_FILE = ROOT / ".env" CLASH_ADDR = "127.0.0.1:9090" _VLESS_INBOUND = "vless-reality-in" +_XRAY_ACCESS_LOG = Path("/var/log/xray/access.log") _LOG_USER_RE = re.compile( r"\[([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\]\s+inbound connection" ) _LOG_INDEX_RE = re.compile(r"\[(\d+)\] inbound connection") +_LOG_XRAY_UUID_RE = re.compile( + r"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})" +) _speed_cache: dict[int, tuple[float, int, int]] = {} _conn_cache: dict[str, dict[str, int | str]] = {} @@ -72,6 +76,26 @@ def fetch_clash_connections() -> tuple[list[dict], bool]: return payload.get("connections") or [], True +def fetch_xray_access_uuids(nodes: list[dict]) -> set[str]: + """VLESS Reality 由 Xray 承载,从 access.log 读取近期活跃 UUID。""" + if not _XRAY_ACCESS_LOG.exists(): + return set() + try: + text = _XRAY_ACCESS_LOG.read_text(encoding="utf-8", errors="ignore") + except OSError: + return set() + known = {node["uuid"] for node in nodes} + active: set[str] = set() + for line in text.splitlines()[-400:]: + if "accepted" not in line: + continue + for match in _LOG_XRAY_UUID_RE.finditer(line): + uid = match.group(1) + if uid in known: + active.add(uid) + return active + + def fetch_recent_log_uuids(nodes: list[dict]) -> set[str]: """sing-box Clash API 不导出 user 字段,VLESS 多用户需从近期日志补全在线 UUID。""" try: @@ -343,6 +367,7 @@ def collect_node_stats() -> dict: uuid_to_node = {node["uuid"]: int(node["id"]) for node in nodes} connections, clash_ok = fetch_clash_connections() log_active = fetch_recent_log_uuids(nodes) if len(nodes) > 1 else set() + log_active |= fetch_xray_access_uuids(nodes) active_by_uuid = _sync_connections(connections, nodes, uuid_to_node, log_active) single_node = len(nodes) == 1 has_connections = len(connections) > 0 diff --git a/scripts/generate-keys.sh b/scripts/generate-keys.sh index 0487015..acf2d00 100644 --- a/scripts/generate-keys.sh +++ b/scripts/generate-keys.sh @@ -68,8 +68,10 @@ if [[ -f "$ENV_FILE" ]]; then fi echo "已写入 $ENV_FILE" echo "" - echo "重要: 密钥已变更,必须重新生成 sing-box 配置并重启:" - echo " python3 ${ROOT_DIR}/scripts/render-server.py && systemctl restart sing-box" + echo "重要: 密钥已变更,必须重新生成配置并重启:" + echo " python3 ${ROOT_DIR}/scripts/render-xray.py" + echo " python3 ${ROOT_DIR}/scripts/render-server.py" + echo " systemctl restart xray sing-box" else echo "提示: 先复制 .env.example 为 .env 并填写 VPS_IP、DOMAIN 等,再重新运行本脚本" >&2 fi diff --git a/scripts/install.sh b/scripts/install.sh index f5d92d8..6958c71 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -184,9 +184,15 @@ python3 -m venv "$ROOT_DIR/panel/venv" log "初始化节点数据库 ..." "$ROOT_DIR/panel/venv/bin/python" "$ROOT_DIR/panel/init_db.py" -log "生成 sing-box 服务端配置 ..." +log "生成 sing-box 服务端配置 (Hysteria2) ..." python3 "$ROOT_DIR/scripts/render-server.py" +log "安装 Xray (VLESS Reality 443) ..." +bash -c "$(curl -fsSL https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install + +log "生成 Xray 服务端配置 ..." +python3 "$ROOT_DIR/scripts/render-xray.py" + log "创建 sing-box systemd 服务 ..." cat > /etc/systemd/system/sing-box.service <<'UNIT' [Unit] @@ -209,7 +215,7 @@ log "创建管理面板 systemd 服务 ..." cat > /etc/systemd/system/jiedian-panel.service </dev/null; then + echo "[+] 安装 Xray ..." + bash -c "$(curl -fsSL https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install +fi + +export JIEDIAN_ROOT="$ROOT_DIR" + +echo "[+] 更新 sing-box 配置(仅 Hysteria2)..." +python3 "$ROOT_DIR/scripts/render-server.py" + +echo "[+] 生成 Xray 配置(VLESS Reality 443)..." +python3 "$ROOT_DIR/scripts/render-xray.py" + +systemctl enable xray 2>/dev/null || true +systemctl restart xray sing-box jiedian-panel + +echo "" +echo "[+] 迁移完成。请运行诊断:" +bash "$ROOT_DIR/scripts/verify-reality.sh" +echo "" +echo "客户端无需改参数,直接测速 VLESS 节点即可。" diff --git a/scripts/render-server.py b/scripts/render-server.py index 61ed711..ff02341 100644 --- a/scripts/render-server.py +++ b/scripts/render-server.py @@ -44,7 +44,6 @@ def load_nodes(db_path: Path) -> list[dict]: def build_config(env: dict[str, str], nodes: list[dict]) -> dict: required = [ - "REALITY_PRIVATE_KEY", "REALITY_SHORT_ID", "REALITY_SERVER_NAME", "DOMAIN", @@ -53,32 +52,9 @@ def build_config(env: dict[str, str], nodes: list[dict]) -> dict: if not env.get(key): raise SystemExit(f".env 缺少 {key}") - vless_users = [{"uuid": n["uuid"], "flow": "xtls-rprx-vision"} for n in nodes] hy2_base_port = 8443 - inbounds: list[dict] = [ - { - "type": "vless", - "tag": "vless-reality-in", - "listen": "0.0.0.0", - "listen_port": 443, - "users": vless_users, - "tls": { - "enabled": True, - "server_name": env["REALITY_SERVER_NAME"], - "reality": { - "enabled": True, - "handshake": { - "server": env["REALITY_SERVER_NAME"], - "server_port": 443, - }, - "private_key": env["REALITY_PRIVATE_KEY"], - "short_id": ["", env["REALITY_SHORT_ID"]], - "max_time_difference": "1m", - }, - }, - }, - ] + inbounds: list[dict] = [] for index, node in enumerate(nodes): inbounds.append( { diff --git a/scripts/render-xray.py b/scripts/render-xray.py new file mode 100644 index 0000000..33d4149 --- /dev/null +++ b/scripts/render-xray.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""根据 data/nodes.db 与 .env 生成 Xray VLESS+Reality 配置(443 端口)。""" +from __future__ import annotations + +import json +import os +import sqlite3 +import subprocess +import sys +from pathlib import Path + +ROOT = Path(os.environ.get("JIEDIAN_ROOT", Path(__file__).resolve().parents[1])) +ENV_FILE = ROOT / ".env" +DB_FILE = ROOT / "data" / "nodes.db" +OUT_FILE = Path("/usr/local/etc/xray/config.json") +ACCESS_LOG = Path("/var/log/xray/access.log") + + +def load_env(path: Path) -> dict[str, str]: + env: dict[str, str] = {} + if not path.exists(): + raise SystemExit(f"缺少 .env: {path}") + for line in path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, value = line.partition("=") + env[key.strip()] = value.strip() + return env + + +def load_nodes(db_path: Path) -> list[dict]: + if not db_path.exists(): + raise SystemExit(f"缺少节点数据库: {db_path},请先运行 install.sh") + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + rows = conn.execute( + "SELECT id, name, uuid, hy2_password FROM nodes WHERE enabled = 1 ORDER BY id" + ).fetchall() + conn.close() + if not rows: + raise SystemExit("没有可用节点,请在管理面板中添加节点") + return [dict(row) for row in rows] + + +def build_config(env: dict[str, str], nodes: list[dict]) -> dict: + required = [ + "REALITY_PRIVATE_KEY", + "REALITY_SHORT_ID", + "REALITY_SERVER_NAME", + ] + for key in required: + if not env.get(key): + raise SystemExit(f".env 缺少 {key}") + + short_id = env["REALITY_SHORT_ID"] + clients = [ + {"id": node["uuid"], "flow": "xtls-rprx-vision", "email": node["uuid"]} + for node in nodes + ] + + return { + "log": { + "access": str(ACCESS_LOG), + "loglevel": "warning", + }, + "inbounds": [ + { + "listen": "0.0.0.0", + "port": 443, + "protocol": "vless", + "settings": { + "clients": clients, + "decryption": "none", + }, + "streamSettings": { + "network": "tcp", + "security": "reality", + "realitySettings": { + "show": False, + "dest": f"{env['REALITY_SERVER_NAME']}:443", + "xver": 0, + "serverNames": [env["REALITY_SERVER_NAME"]], + "privateKey": env["REALITY_PRIVATE_KEY"], + "shortIds": ["", short_id], + }, + }, + "sniffing": { + "enabled": True, + "destOverride": ["http", "tls", "quic"], + }, + } + ], + "outbounds": [ + {"protocol": "freedom", "tag": "direct"}, + {"protocol": "blackhole", "tag": "block"}, + ], + "routing": { + "rules": [ + { + "type": "field", + "ip": ["geoip:private"], + "outboundTag": "block", + } + ] + }, + } + + +def main() -> None: + env = load_env(ENV_FILE) + nodes = load_nodes(DB_FILE) + config = build_config(env, nodes) + + OUT_FILE.parent.mkdir(parents=True, exist_ok=True) + ACCESS_LOG.parent.mkdir(parents=True, exist_ok=True) + OUT_FILE.write_text(json.dumps(config, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + + xray = subprocess.run( + ["xray", "run", "-test", "-c", str(OUT_FILE)], + capture_output=True, + text=True, + ) + if xray.returncode != 0: + sys.stderr.write(xray.stderr or xray.stdout) + raise SystemExit(xray.returncode) + + print(f"已生成 {OUT_FILE}({len(nodes)} 个 VLESS 用户)") + + +if __name__ == "__main__": + main() diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index f85b1bf..bd07ef1 100644 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -6,16 +6,17 @@ set -euo pipefail [[ $EUID -eq 0 ]] || { echo "请使用 root 运行"; exit 1; } echo "[*] 停止服务 ..." -systemctl stop jiedian-panel sing-box 2>/dev/null || true -systemctl disable jiedian-panel sing-box 2>/dev/null || true +systemctl stop jiedian-panel xray sing-box 2>/dev/null || true +systemctl disable jiedian-panel xray sing-box 2>/dev/null || true echo "[*] 删除 systemd 单元 ..." rm -f /etc/systemd/system/jiedian-panel.service rm -f /etc/systemd/system/sing-box.service systemctl daemon-reload -echo "[*] 删除 sing-box 配置 ..." +echo "[*] 删除 sing-box / Xray 配置 ..." rm -rf /etc/sing-box +rm -f /usr/local/etc/xray/config.json echo "[*] 删除 nginx 站点 ..." rm -f /etc/nginx/sites-enabled/panel diff --git a/scripts/verify-reality.sh b/scripts/verify-reality.sh new file mode 100644 index 0000000..1703586 --- /dev/null +++ b/scripts/verify-reality.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# 核对 Reality 密钥是否一致,并验证 Xray 配置 +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ENV_FILE="${ROOT}/.env" +XRAY_CFG="/usr/local/etc/xray/config.json" +SB_CFG="/etc/sing-box/config.json" + +[[ -f "$ENV_FILE" ]] || { echo "缺少 $ENV_FILE"; exit 1; } +# shellcheck disable=SC1090 +source "$ENV_FILE" + +echo "========== .env ==========" +grep ^REALITY_ "$ENV_FILE" | grep -v PRIVATE || true +echo "REALITY_PRIVATE_KEY=***(已隐藏)" + +if command -v xray &>/dev/null && [[ -f "$XRAY_CFG" ]]; then + echo "" + echo "========== Xray config.json ==========" + ENV_FILE="$ENV_FILE" XRAY_CFG="$XRAY_CFG" python3 - <<'PY' +import json, os +from pathlib import Path +env = {} +for line in Path(os.environ["ENV_FILE"]).read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + k, _, v = line.partition("=") + env[k.strip()] = v.strip() +cfg = json.load(open(os.environ["XRAY_CFG"])) +rs = cfg["inbounds"][0]["streamSettings"]["realitySettings"] +pk = rs["privateKey"] +print("privateKey:", pk[:8] + "..." if pk else "(empty)") +print("shortIds:", rs.get("shortIds")) +print("serverNames:", rs.get("serverNames")) +print("clients:", len(cfg["inbounds"][0]["settings"]["clients"])) +match = pk == env.get("REALITY_PRIVATE_KEY", "") +print("privateKey 与 .env 一致:", "是" if match else "否 ← 需运行 render-xray.py") +PY + echo "" + echo "========== xray -test ==========" + xray run -test -c "$XRAY_CFG" +else + echo "" + echo "Xray 未安装或配置不存在。VLESS Reality 需 Xray:" + echo " bash -c \"\$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)\" @ install" + echo " python3 ${ROOT}/scripts/render-xray.py" +fi + +if [[ -f "$SB_CFG" ]] && grep -q vless-reality "$SB_CFG" 2>/dev/null; then + echo "" + echo "[!] sing-box 仍含 vless-reality inbound,会与 Xray 争抢 443。" + echo " 请运行: python3 ${ROOT}/scripts/render-server.py && systemctl restart sing-box" +fi + +if command -v xray &>/dev/null && [[ -n "${REALITY_PRIVATE_KEY:-}" ]]; then + echo "" + echo "========== 公钥配对 ==========" + DERIVED="$(xray x25519 -i "$REALITY_PRIVATE_KEY" 2>/dev/null | awk '/Public key/ {print $3}')" + if [[ -n "$DERIVED" ]]; then + if [[ "$DERIVED" == "${REALITY_PUBLIC_KEY:-}" ]]; then + echo "公钥与私钥配对: 是" + else + echo "公钥与私钥配对: 否" + echo " .env PUBLIC: ${REALITY_PUBLIC_KEY:-}" + echo " 推导 PUBLIC: $DERIVED" + echo " 请重新运行 generate-keys.sh 并 render-xray.py" + fi + fi +fi