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 <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-16 11:56:22 +08:00
parent 5685b869dc
commit c9895133cb
9 changed files with 305 additions and 39 deletions
+27 -5
View File
@@ -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。"""
+25
View File
@@ -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
+4 -2
View File
@@ -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
+11 -4
View File
@@ -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 <<UNIT
[Unit]
Description=jiedian admin panel
After=network.target sing-box.service
After=network.target xray.service sing-box.service
[Service]
Type=simple
@@ -226,7 +232,7 @@ WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable sing-box jiedian-panel
systemctl enable xray sing-box jiedian-panel
log "注册证书续期 reload 命令 ..."
/root/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \
@@ -235,7 +241,7 @@ log "注册证书续期 reload 命令 ..."
--reloadcmd "systemctl restart sing-box && systemctl reload nginx" \
|| log "acme reloadcmd 注册失败,可忽略"
systemctl restart sing-box jiedian-panel
systemctl restart xray sing-box jiedian-panel
log "部署完成!"
echo ""
@@ -249,5 +255,6 @@ echo ""
echo "节点链接请在面板中添加/复制。"
echo ""
log "sing-box: systemctl status sing-box"
log "Xray: systemctl status xray"
log "面板: systemctl status jiedian-panel"
log "卸载重装: bash scripts/uninstall.sh && bash scripts/install.sh"
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# 已有 VPS:将 VLESS Reality 从 sing-box 迁移到 Xray443
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
[[ $EUID -eq 0 ]] || { echo "请使用 root 运行"; exit 1; }
if ! command -v xray &>/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 节点即可。"
+1 -25
View File
@@ -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(
{
+132
View File
@@ -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()
+4 -3
View File
@@ -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
+71
View File
@@ -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