fix: multi-node online stats, per-node Hy2 ports, and panel reload stability

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-16 11:35:15 +08:00
parent 33533d7ebc
commit abbaac9520
11 changed files with 246 additions and 73 deletions
+21 -1
View File
@@ -74,6 +74,26 @@ systemctl restart jiedian-panel
若域名在阿里云/Cloudflare 开了 **CDN 代理**,建议对管理域名 **关闭 CDN**(仅 DNS 解析到 VPS),否则 80 端口回源也可能异常。 若域名在阿里云/Cloudflare 开了 **CDN 代理**,建议对管理域名 **关闭 CDN**(仅 DNS 解析到 VPS),否则 80 端口回源也可能异常。
### 多节点全部显示离线
sing-box 的 Clash API **不会在连接详情里返回用户 UUID**(单节点时面板用兜底逻辑,多节点必须按 inbound 区分)。
升级后每个节点有 **独立 Hysteria2 端口**(按节点 ID 排序:8443、8444、8445…)。请:
1. 更新代码并重新生成配置:
```bash
cd /opt/jiedian && git pull
python3 scripts/render-server.py
systemctl restart sing-box jiedian-panel
```
2. **阿里云安全组** 放行 `8443-8499/UDP`(不只 8443
3. 在面板 **重新复制** 各节点的 Hysteria2 链接(端口可能已变)
4. VLESS 多用户在线状态依赖 sing-box 日志,需保持 `log.level` 为 `info`render-server 已默认)
### 添加/删除节点后白屏或 503
创建节点会触发 sing-box 重启,页面刷新时可能短暂不可用。新版前端会自动重试;若仍白屏,等 10 秒后手动刷新 `http://域名/面板路径/`。
### 在线节点 / 统计一直显示「检测中」 ### 在线节点 / 统计一直显示「检测中」
页面初始状态是「检测中」。若长期不变且数字一直是 `-`,说明 **前端 JS 或 `/api/stats` 请求失败**(常见:静态资源路径缺少 `PANEL_PATH` 前缀)。 页面初始状态是「检测中」。若长期不变且数字一直是 `-`,说明 **前端 JS 或 `/api/stats` 请求失败**(常见:静态资源路径缺少 `PANEL_PATH` 前缀)。
@@ -290,7 +310,7 @@ Hysteria2 仍需单独部署(或使用 sing-box 仅跑 Hy2 inbound)。
| 22 | TCP | SSH | 是 | | 22 | TCP | SSH | 是 |
| 80 | TCP | ACME 验证 + **管理面板** | 是 | | 80 | TCP | ACME 验证 + **管理面板** | 是 |
| 443 | TCP | VLESS + Reality | 是 | | 443 | TCP | VLESS + Reality | 是 |
| 8443 | UDP | Hysteria2 | 是 | | 8443-8499 | UDP | Hysteria2(每节点递增端口) | 是 |
| ~~8444~~ | ~~TCP~~ | ~~旧版面板的独立 HTTPS~~ | **已废弃,无需放行** | | ~~8444~~ | ~~TCP~~ | ~~旧版面板的独立 HTTPS~~ | **已废弃,无需放行** |
## 安全建议 ## 安全建议
+23 -8
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
import os import os
import secrets import secrets
import subprocess import subprocess
import threading
from functools import wraps from functools import wraps
from pathlib import Path from pathlib import Path
@@ -26,6 +27,7 @@ from stats import collect_node_stats
ROOT = Path(os.environ.get("JIEDIAN_ROOT", Path(__file__).resolve().parents[1])) ROOT = Path(os.environ.get("JIEDIAN_ROOT", Path(__file__).resolve().parents[1]))
SECRET_FILE = ROOT / "data" / ".panel_secret" SECRET_FILE = ROOT / "data" / ".panel_secret"
RENDER_SCRIPT = ROOT / "scripts" / "render-server.py" RENDER_SCRIPT = ROOT / "scripts" / "render-server.py"
_apply_lock = threading.Lock()
def _secret_key() -> str: def _secret_key() -> str:
@@ -128,6 +130,18 @@ def apply_singbox() -> tuple[bool, str]:
return True, "ok" return True, "ok"
def apply_singbox_background(on_fail=None) -> None:
"""后台生成配置并重启 sing-box,避免阻塞 HTTP 请求导致 Nginx 503。"""
def worker() -> None:
with _apply_lock:
ok, msg = apply_singbox()
if not ok and on_fail:
on_fail(msg)
threading.Thread(target=worker, daemon=True).start()
@app.route("/login", methods=["GET", "POST"]) @app.route("/login", methods=["GET", "POST"])
def login(): def login():
if session.get("user"): if session.get("user"):
@@ -196,16 +210,19 @@ def api_add_node():
body = request.get_json(silent=True) or {} body = request.get_json(silent=True) or {}
name = (body.get("name") or request.form.get("name") or "新节点").strip() name = (body.get("name") or request.form.get("name") or "新节点").strip()
node = add_node(name) node = add_node(name)
ok, msg = apply_singbox()
if not ok:
delete_node(node["id"])
return jsonify({"error": msg}), 500
env = load_env() env = load_env()
node_id = int(node["id"])
def on_fail(_msg: str) -> None:
delete_node(node_id)
apply_singbox_background(on_fail=on_fail)
return jsonify( return jsonify(
{ {
"id": node["id"], "id": node["id"],
"name": node["name"], "name": node["name"],
"links": build_links(node, env), "links": build_links(node, env),
"pending": True,
} }
) )
@@ -217,11 +234,9 @@ def api_delete_node(node_id: int):
return jsonify({"error": "至少保留一个节点"}), 400 return jsonify({"error": "至少保留一个节点"}), 400
if not delete_node(node_id): if not delete_node(node_id):
return jsonify({"error": "节点不存在"}), 404 return jsonify({"error": "节点不存在"}), 404
ok, msg = apply_singbox() apply_singbox_background()
if not ok:
return jsonify({"error": msg}), 500
return jsonify({"ok": True}) return jsonify({"ok": True})
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="127.0.0.1", port=5080) app.run(host="127.0.0.1", port=5080, threaded=True)
+2 -4
View File
@@ -6,7 +6,7 @@ import hmac
import os import os
import secrets import secrets
import sqlite3 import sqlite3
import subprocess import uuid
from pathlib import Path from pathlib import Path
ROOT = Path(os.environ.get("JIEDIAN_ROOT", Path(__file__).resolve().parents[1])) ROOT = Path(os.environ.get("JIEDIAN_ROOT", Path(__file__).resolve().parents[1]))
@@ -156,6 +156,4 @@ def node_count() -> int:
def _generate_credentials() -> tuple[str, str]: def _generate_credentials() -> tuple[str, str]:
uuid = subprocess.check_output(["sing-box", "generate", "uuid"], text=True).strip() return str(uuid.uuid4()), secrets.token_urlsafe(18)[:24]
hy2 = secrets.token_urlsafe(18)[:24]
return uuid, hy2
+5 -1
View File
@@ -5,6 +5,9 @@ import os
from pathlib import Path from pathlib import Path
from urllib.parse import quote from urllib.parse import quote
from db import list_nodes
from nodes_util import hy2_port
def load_env() -> dict[str, str]: def load_env() -> dict[str, str]:
root = Path(os.environ.get("JIEDIAN_ROOT", Path(__file__).resolve().parents[1])) root = Path(os.environ.get("JIEDIAN_ROOT", Path(__file__).resolve().parents[1]))
@@ -26,6 +29,7 @@ def build_links(node: dict, env: dict | None = None) -> dict[str, str]:
public_key = env["REALITY_PUBLIC_KEY"] public_key = env["REALITY_PUBLIC_KEY"]
short_id = env["REALITY_SHORT_ID"] short_id = env["REALITY_SHORT_ID"]
name = quote(node["name"]) name = quote(node["name"])
port = hy2_port(node, list_nodes())
vless = ( vless = (
f"vless://{node['uuid']}@{vps_ip}:443" f"vless://{node['uuid']}@{vps_ip}:443"
@@ -33,5 +37,5 @@ def build_links(node: dict, env: dict | None = None) -> dict[str, str]:
f"&sni={reality_sni}&fp=chrome&pbk={public_key}&sid={short_id}" f"&sni={reality_sni}&fp=chrome&pbk={public_key}&sid={short_id}"
f"&type=tcp#{name}" f"&type=tcp#{name}"
) )
hy2 = f"hy2://{node['hy2_password']}@{domain}:8443?sni={domain}#{name}-Hy2" hy2 = f"hy2://{node['hy2_password']}@{domain}:{port}?sni={domain}#{name}-Hy2"
return {"vless": vless, "hy2": hy2} return {"vless": vless, "hy2": hy2}
+16
View File
@@ -0,0 +1,16 @@
"""节点端口与 inbound 标签(多节点统计用)。"""
def ordered_nodes(nodes: list[dict]) -> list[dict]:
return sorted(nodes, key=lambda n: int(n["id"]))
def hy2_port(node: dict, nodes: list[dict], base: int = 8443) -> int:
for index, item in enumerate(ordered_nodes(nodes)):
if int(item["id"]) == int(node["id"]):
return base + index
return base
def hy2_inbound_tag(node_id: int) -> str:
return f"hy2-in-{node_id}"
+32 -4
View File
@@ -102,6 +102,33 @@ function setButtonBusy(btn, busy, busyText) {
} }
} }
async function readJson(res) {
const text = await res.text();
try {
return JSON.parse(text);
} catch {
throw new Error(res.ok ? "响应格式错误" : `HTTP ${res.status}`);
}
}
async function reloadPanel(maxWaitMs = 15000) {
const url = `${panelBase()}/`;
const started = Date.now();
while (Date.now() - started < maxWaitMs) {
await new Promise((r) => setTimeout(r, 1500));
try {
const res = await fetch(url, { credentials: "same-origin" });
if (res.ok) {
location.href = url;
return;
}
} catch {
/* sing-box 重启期间可能短暂不可用,继续重试 */
}
}
location.href = url;
}
if (confirmAddBtn) { if (confirmAddBtn) {
confirmAddBtn.addEventListener("click", async () => { confirmAddBtn.addEventListener("click", async () => {
const name = nodeName.value.trim() || "新节点"; const name = nodeName.value.trim() || "新节点";
@@ -114,10 +141,11 @@ if (confirmAddBtn) {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }), body: JSON.stringify({ name }),
}); });
const data = await res.json(); const data = await readJson(res);
if (!res.ok) throw new Error(data.error || "创建失败"); if (!res.ok) throw new Error(data.error || "创建失败");
toast("节点已创建,配置生效中…"); toast("节点已创建,配置生效中…");
setTimeout(() => location.reload(), 600); modal.classList.add("hidden");
reloadPanel();
} catch (err) { } catch (err) {
toast(err.message || "创建失败"); toast(err.message || "创建失败");
setButtonBusy(confirmAddBtn, false); setButtonBusy(confirmAddBtn, false);
@@ -136,10 +164,10 @@ document.querySelectorAll(".delete-btn").forEach((btn) => {
method: "DELETE", method: "DELETE",
credentials: "same-origin", credentials: "same-origin",
}); });
const data = await res.json(); const data = await readJson(res);
if (!res.ok) throw new Error(data.error || "删除失败"); if (!res.ok) throw new Error(data.error || "删除失败");
toast("已删除,配置生效中…"); toast("已删除,配置生效中…");
setTimeout(() => location.reload(), 600); reloadPanel();
} catch (err) { } catch (err) {
toast(err.message || "删除失败"); toast(err.message || "删除失败");
setButtonBusy(btn, false); setButtonBusy(btn, false);
+108 -24
View File
@@ -3,18 +3,26 @@ from __future__ import annotations
import json import json
import os import os
import re
import sqlite3 import sqlite3
import subprocess
import time import time
import urllib.error import urllib.error
import urllib.request import urllib.request
from pathlib import Path from pathlib import Path
from db import connect, list_nodes from db import connect, list_nodes
from nodes_util import hy2_inbound_tag, ordered_nodes
ROOT = Path(os.environ.get("JIEDIAN_ROOT", Path(__file__).resolve().parents[1])) ROOT = Path(os.environ.get("JIEDIAN_ROOT", Path(__file__).resolve().parents[1]))
ENV_FILE = ROOT / ".env" ENV_FILE = ROOT / ".env"
CLASH_ADDR = "127.0.0.1:9090" CLASH_ADDR = "127.0.0.1:9090"
_VLESS_INBOUND = "vless-reality-in"
_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")
_speed_cache: dict[int, tuple[float, int, int]] = {} _speed_cache: dict[int, tuple[float, int, int]] = {}
_conn_cache: dict[str, dict[str, int | str]] = {} _conn_cache: dict[str, dict[str, int | str]] = {}
@@ -64,6 +72,43 @@ def fetch_clash_connections() -> tuple[list[dict], bool]:
return payload.get("connections") or [], True return payload.get("connections") or [], True
def fetch_recent_log_uuids(nodes: list[dict]) -> set[str]:
"""sing-box Clash API 不导出 user 字段,VLESS 多用户需从近期日志补全在线 UUID。"""
try:
proc = subprocess.run(
[
"journalctl",
"-u",
"sing-box",
"--since",
"3 min ago",
"--no-pager",
"-o",
"cat",
],
capture_output=True,
text=True,
timeout=2,
)
except (OSError, subprocess.TimeoutExpired):
return set()
if proc.returncode != 0:
return set()
known = {node["uuid"] for node in nodes}
index_to_uuid = {i: node["uuid"] for i, node in enumerate(ordered_nodes(nodes))}
active: set[str] = set()
for match in _LOG_USER_RE.finditer(proc.stdout):
uid = match.group(1)
if uid in known:
active.add(uid)
for match in _LOG_INDEX_RE.finditer(proc.stdout):
uid = index_to_uuid.get(int(match.group(1)))
if uid:
active.add(uid)
return active
def _global_conn_speed(connections: list[dict]) -> tuple[float, float]: def _global_conn_speed(connections: list[dict]) -> tuple[float, float]:
"""从 /connections 汇总字节增量估算全局速率(/traffic 为 WebSocket 流,不能同步 HTTP 读)。""" """从 /connections 汇总字节增量估算全局速率(/traffic 为 WebSocket 流,不能同步 HTTP 读)。"""
total_up = sum(int(c.get("upload") or 0) for c in connections) total_up = sum(int(c.get("upload") or 0) for c in connections)
@@ -81,6 +126,14 @@ def _global_conn_speed(connections: list[dict]) -> tuple[float, float]:
return max(0.0, (total_up - u0) / dt), max(0.0, (total_down - d0) / dt) return max(0.0, (total_up - u0) / dt), max(0.0, (total_down - d0) / dt)
def _connection_inbound_tag(conn: dict) -> str:
meta = conn.get("metadata") or {}
inbound_type = str(meta.get("type") or "")
if "/" in inbound_type:
return inbound_type.split("/", 1)[1]
return inbound_type
def _connection_user(conn: dict) -> str: def _connection_user(conn: dict) -> str:
meta = conn.get("metadata") or {} meta = conn.get("metadata") or {}
for key in ("user", "uid", "auth_user", "auth", "username"): for key in ("user", "uid", "auth_user", "auth", "username"):
@@ -94,17 +147,6 @@ def _connection_user(conn: dict) -> str:
return "" return ""
def _connection_meta(conn: dict) -> str:
meta = conn.get("metadata") or {}
parts = [
str(meta.get("type") or ""),
str(meta.get("network") or ""),
str(meta.get("inbound") or meta.get("inboundTag") or ""),
str(meta.get("inboundType") or ""),
]
return " ".join(parts).lower()
def _node_auth_keys(node: dict) -> set[str]: def _node_auth_keys(node: dict) -> set[str]:
keys = {node["uuid"]} keys = {node["uuid"]}
if node.get("hy2_password"): if node.get("hy2_password"):
@@ -112,18 +154,32 @@ def _node_auth_keys(node: dict) -> set[str]:
return keys return keys
def _match_connection(conn: dict, node: dict) -> bool: def _match_connection(conn: dict, node: dict, *, single_node: bool = False) -> bool:
user = _connection_user(conn) user = _connection_user(conn)
if user and user in _node_auth_keys(node): if user and user in _node_auth_keys(node):
return True return True
meta = _connection_meta(conn)
if "hysteria" in meta and node.get("hy2_password"): tag = _connection_inbound_tag(conn)
# 旧配置未设置 hy2 name 时,Clash API 可能不带 user node_id = int(node["id"])
if user == node["hy2_password"]: expected_hy2 = hy2_inbound_tag(node_id)
return True if tag == expected_hy2:
return True
if tag == "hysteria2-in" and single_node:
return True
return False return False
def _match_vless_connection(conn: dict, node: dict, log_active: set[str]) -> bool:
tag = _connection_inbound_tag(conn)
if tag != _VLESS_INBOUND:
return False
user = _connection_user(conn)
if user == node["uuid"]:
return True
# 共享 VLESS inbound 无法从 Clash API 区分用户;仅唯一活跃用户时归因
return node["uuid"] in log_active and len(log_active) == 1
def _connection_id(conn: dict) -> str: def _connection_id(conn: dict) -> str:
return str(conn.get("id") or "") return str(conn.get("id") or "")
@@ -185,6 +241,7 @@ def _sync_connections(
connections: list[dict], connections: list[dict],
nodes: list[dict], nodes: list[dict],
uuid_to_node: dict[str, int], uuid_to_node: dict[str, int],
log_active: set[str],
) -> dict[str, tuple[int, int]]: ) -> dict[str, tuple[int, int]]:
"""同步连接缓存,断开连接时写入累计流量,返回各用户当前活跃会话流量。""" """同步连接缓存,断开连接时写入累计流量,返回各用户当前活跃会话流量。"""
seen: set[str] = set() seen: set[str] = set()
@@ -195,7 +252,9 @@ def _sync_connections(
for conn in connections: for conn in connections:
matched_uuid = "" matched_uuid = ""
for node in nodes: for node in nodes:
if _match_connection(conn, node): if _match_connection(conn, node, single_node=single_node) or _match_vless_connection(
conn, node, log_active
):
matched_uuid = node["uuid"] matched_uuid = node["uuid"]
break break
if not matched_uuid and single_node and connections: if not matched_uuid and single_node and connections:
@@ -252,11 +311,39 @@ def _calc_speed(node_id: int, up: int, down: int) -> tuple[float, float]:
return max(0.0, (up - u0) / dt), max(0.0, (down - d0) / dt) return max(0.0, (up - u0) / dt), max(0.0, (down - d0) / dt)
def _connections_for_node(
connections: list[dict],
node: dict,
nodes: list[dict],
log_active: set[str],
) -> list[dict | None]:
single_node = len(nodes) == 1
matched = [
c
for c in connections
if _match_connection(c, node, single_node=single_node)
or _match_vless_connection(c, node, log_active)
]
if matched:
return matched
if single_node and connections:
return connections
if node["uuid"] in log_active:
vless_hits = [c for c in connections if _connection_inbound_tag(c) == _VLESS_INBOUND]
if vless_hits:
return vless_hits
return [None]
return []
def collect_node_stats() -> dict: def collect_node_stats() -> dict:
nodes = list_nodes() nodes = list_nodes()
uuid_to_node = {node["uuid"]: int(node["id"]) for node in nodes} uuid_to_node = {node["uuid"]: int(node["id"]) for node in nodes}
connections, clash_ok = fetch_clash_connections() connections, clash_ok = fetch_clash_connections()
active_by_uuid = _sync_connections(connections, nodes, uuid_to_node) log_active = fetch_recent_log_uuids(nodes) if len(nodes) > 1 else set()
active_by_uuid = _sync_connections(connections, nodes, uuid_to_node, log_active)
single_node = len(nodes) == 1 single_node = len(nodes) == 1
has_connections = len(connections) > 0 has_connections = len(connections) > 0
global_up_speed, global_down_speed = _global_conn_speed(connections) if clash_ok else (0.0, 0.0) global_up_speed, global_down_speed = _global_conn_speed(connections) if clash_ok else (0.0, 0.0)
@@ -276,11 +363,7 @@ def collect_node_stats() -> dict:
display_down = stored_down + session_down display_down = stored_down + session_down
up_speed, down_speed = _calc_speed(node_id, display_up, display_down) up_speed, down_speed = _calc_speed(node_id, display_up, display_down)
matched = [c for c in connections if _match_connection(c, node)] matched = _connections_for_node(connections, node, nodes, log_active)
if not matched and single_node and has_connections:
matched = connections
if not matched and (session_up + session_down) > 0:
matched = [None] # 有活跃会话但 Clash 未返回连接详情
if not matched and single_node and global_active: if not matched and single_node and global_active:
up_speed = global_up_speed up_speed = global_up_speed
down_speed = global_down_speed down_speed = global_down_speed
@@ -288,6 +371,7 @@ def collect_node_stats() -> dict:
online = ( online = (
len(matched) > 0 len(matched) > 0
or (session_up + session_down) > 0 or (session_up + session_down) > 0
or uid in log_active
or (up_speed + down_speed) > 512 or (up_speed + down_speed) > 512
or (single_node and (global_active or has_connections)) or (single_node and (global_active or has_connections))
) )
+1 -1
View File
@@ -13,7 +13,7 @@
<section class="hero"> <section class="hero">
<div> <div>
<h1>节点列表</h1> <h1>节点列表</h1>
<p class="muted">VPS {{ vps_ip }} · Reality 443 · Hysteria2 8443</p> <p class="muted">VPS {{ vps_ip }} · Reality 443 · Hysteria2 8443+</p>
</div> </div>
<button id="addBtn" class="btn primary">+ 添加节点</button> <button id="addBtn" class="btn primary">+ 添加节点</button>
</section> </section>
+1 -1
View File
@@ -127,7 +127,7 @@ ufw default allow outgoing
ufw allow 22/tcp comment 'SSH' ufw allow 22/tcp comment 'SSH'
ufw allow 80/tcp comment 'HTTP-ACME-Panel' ufw allow 80/tcp comment 'HTTP-ACME-Panel'
ufw allow 443/tcp comment 'Reality' ufw allow 443/tcp comment 'Reality'
ufw allow 8443/udp comment 'Hysteria2' ufw allow 8443:8499/udp comment 'Hysteria2-multi-node'
ufw --force enable ufw --force enable
log "部署 Nginx fallback 站点 ..." log "部署 Nginx fallback 站点 ..."
+34 -29
View File
@@ -54,49 +54,54 @@ def build_config(env: dict[str, str], nodes: list[dict]) -> dict:
raise SystemExit(f".env 缺少 {key}") raise SystemExit(f".env 缺少 {key}")
vless_users = [{"uuid": n["uuid"], "flow": "xtls-rprx-vision"} for n in nodes] vless_users = [{"uuid": n["uuid"], "flow": "xtls-rprx-vision"} for n in nodes]
# name 与 VLESS uuid 一致,便于 v2ray/clash API 按用户统计流量 hy2_base_port = 8443
hy2_users = [
{"name": n["uuid"], "password": n["hy2_password"]} for n in nodes
]
clash_secret = env.get("CLASH_API_SECRET", "")
config = { inbounds: list[dict] = [
"log": {"level": "warn", "timestamp": True}, {
"inbounds": [ "type": "vless",
{ "tag": "vless-reality-in",
"type": "vless", "listen": "0.0.0.0",
"tag": "vless-reality-in", "listen_port": 443,
"listen": "0.0.0.0", "users": vless_users,
"listen_port": 443, "tls": {
"users": vless_users, "enabled": True,
"tls": { "server_name": env["REALITY_SERVER_NAME"],
"reality": {
"enabled": True, "enabled": True,
"server_name": env["REALITY_SERVER_NAME"], "handshake": {
"reality": { "server": env["REALITY_SERVER_NAME"],
"enabled": True, "server_port": 443,
"handshake": {
"server": env["REALITY_SERVER_NAME"],
"server_port": 443,
},
"private_key": env["REALITY_PRIVATE_KEY"],
"short_id": [env["REALITY_SHORT_ID"]],
}, },
"private_key": env["REALITY_PRIVATE_KEY"],
"short_id": [env["REALITY_SHORT_ID"]],
}, },
}, },
},
]
for index, node in enumerate(nodes):
inbounds.append(
{ {
"type": "hysteria2", "type": "hysteria2",
"tag": "hysteria2-in", "tag": f"hy2-in-{node['id']}",
"listen": "0.0.0.0", "listen": "0.0.0.0",
"listen_port": 8443, "listen_port": hy2_base_port + index,
"users": hy2_users, "users": [
{"name": node["uuid"], "password": node["hy2_password"]},
],
"tls": { "tls": {
"enabled": True, "enabled": True,
"server_name": env["DOMAIN"], "server_name": env["DOMAIN"],
"certificate_path": "/etc/sing-box/certs/fullchain.pem", "certificate_path": "/etc/sing-box/certs/fullchain.pem",
"key_path": "/etc/sing-box/certs/privkey.pem", "key_path": "/etc/sing-box/certs/privkey.pem",
}, },
}, }
], )
clash_secret = env.get("CLASH_API_SECRET", "")
config = {
"log": {"level": "info", "timestamp": True},
"inbounds": inbounds,
"outbounds": [{"type": "direct", "tag": "direct"}], "outbounds": [{"type": "direct", "tag": "direct"}],
"route": { "route": {
"rules": [{"ip_is_private": True, "action": "reject"}], "rules": [{"ip_is_private": True, "action": "reject"}],
+3
View File
@@ -20,6 +20,9 @@ server {
__PANEL_ALLOW__ __PANEL_ALLOW__
proxy_pass http://127.0.0.1:5080/; proxy_pass http://127.0.0.1:5080/;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_connect_timeout 10s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
proxy_set_header Host __DOMAIN__; proxy_set_header Host __DOMAIN__;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;