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:
+21
-1
@@ -74,6 +74,26 @@ systemctl restart jiedian-panel
|
||||
|
||||
若域名在阿里云/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` 前缀)。
|
||||
@@ -290,7 +310,7 @@ Hysteria2 仍需单独部署(或使用 sing-box 仅跑 Hy2 inbound)。
|
||||
| 22 | TCP | SSH | 是 |
|
||||
| 80 | TCP | ACME 验证 + **管理面板** | 是 |
|
||||
| 443 | TCP | VLESS + Reality | 是 |
|
||||
| 8443 | UDP | Hysteria2 | 是 |
|
||||
| 8443-8499 | UDP | Hysteria2(每节点递增端口) | 是 |
|
||||
| ~~8444~~ | ~~TCP~~ | ~~旧版面板的独立 HTTPS~~ | **已废弃,无需放行** |
|
||||
|
||||
## 安全建议
|
||||
|
||||
+23
-8
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import os
|
||||
import secrets
|
||||
import subprocess
|
||||
import threading
|
||||
from functools import wraps
|
||||
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]))
|
||||
SECRET_FILE = ROOT / "data" / ".panel_secret"
|
||||
RENDER_SCRIPT = ROOT / "scripts" / "render-server.py"
|
||||
_apply_lock = threading.Lock()
|
||||
|
||||
|
||||
def _secret_key() -> str:
|
||||
@@ -128,6 +130,18 @@ def apply_singbox() -> tuple[bool, str]:
|
||||
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"])
|
||||
def login():
|
||||
if session.get("user"):
|
||||
@@ -196,16 +210,19 @@ def api_add_node():
|
||||
body = request.get_json(silent=True) or {}
|
||||
name = (body.get("name") or request.form.get("name") or "新节点").strip()
|
||||
node = add_node(name)
|
||||
ok, msg = apply_singbox()
|
||||
if not ok:
|
||||
delete_node(node["id"])
|
||||
return jsonify({"error": msg}), 500
|
||||
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(
|
||||
{
|
||||
"id": node["id"],
|
||||
"name": node["name"],
|
||||
"links": build_links(node, env),
|
||||
"pending": True,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -217,11 +234,9 @@ def api_delete_node(node_id: int):
|
||||
return jsonify({"error": "至少保留一个节点"}), 400
|
||||
if not delete_node(node_id):
|
||||
return jsonify({"error": "节点不存在"}), 404
|
||||
ok, msg = apply_singbox()
|
||||
if not ok:
|
||||
return jsonify({"error": msg}), 500
|
||||
apply_singbox_background()
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
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
@@ -6,7 +6,7 @@ import hmac
|
||||
import os
|
||||
import secrets
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
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]:
|
||||
uuid = subprocess.check_output(["sing-box", "generate", "uuid"], text=True).strip()
|
||||
hy2 = secrets.token_urlsafe(18)[:24]
|
||||
return uuid, hy2
|
||||
return str(uuid.uuid4()), secrets.token_urlsafe(18)[:24]
|
||||
|
||||
+5
-1
@@ -5,6 +5,9 @@ import os
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
from db import list_nodes
|
||||
from nodes_util import hy2_port
|
||||
|
||||
|
||||
def load_env() -> dict[str, str]:
|
||||
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"]
|
||||
short_id = env["REALITY_SHORT_ID"]
|
||||
name = quote(node["name"])
|
||||
port = hy2_port(node, list_nodes())
|
||||
|
||||
vless = (
|
||||
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"&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}
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
confirmAddBtn.addEventListener("click", async () => {
|
||||
const name = nodeName.value.trim() || "新节点";
|
||||
@@ -114,10 +141,11 @@ if (confirmAddBtn) {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
const data = await res.json();
|
||||
const data = await readJson(res);
|
||||
if (!res.ok) throw new Error(data.error || "创建失败");
|
||||
toast("节点已创建,配置生效中…");
|
||||
setTimeout(() => location.reload(), 600);
|
||||
modal.classList.add("hidden");
|
||||
reloadPanel();
|
||||
} catch (err) {
|
||||
toast(err.message || "创建失败");
|
||||
setButtonBusy(confirmAddBtn, false);
|
||||
@@ -136,10 +164,10 @@ document.querySelectorAll(".delete-btn").forEach((btn) => {
|
||||
method: "DELETE",
|
||||
credentials: "same-origin",
|
||||
});
|
||||
const data = await res.json();
|
||||
const data = await readJson(res);
|
||||
if (!res.ok) throw new Error(data.error || "删除失败");
|
||||
toast("已删除,配置生效中…");
|
||||
setTimeout(() => location.reload(), 600);
|
||||
reloadPanel();
|
||||
} catch (err) {
|
||||
toast(err.message || "删除失败");
|
||||
setButtonBusy(btn, false);
|
||||
|
||||
+108
-24
@@ -3,18 +3,26 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
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]))
|
||||
ENV_FILE = ROOT / ".env"
|
||||
|
||||
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]] = {}
|
||||
_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
|
||||
|
||||
|
||||
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]:
|
||||
"""从 /connections 汇总字节增量估算全局速率(/traffic 为 WebSocket 流,不能同步 HTTP 读)。"""
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
meta = conn.get("metadata") or {}
|
||||
for key in ("user", "uid", "auth_user", "auth", "username"):
|
||||
@@ -94,17 +147,6 @@ def _connection_user(conn: dict) -> str:
|
||||
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]:
|
||||
keys = {node["uuid"]}
|
||||
if node.get("hy2_password"):
|
||||
@@ -112,18 +154,32 @@ def _node_auth_keys(node: dict) -> set[str]:
|
||||
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)
|
||||
if user and user in _node_auth_keys(node):
|
||||
return True
|
||||
meta = _connection_meta(conn)
|
||||
if "hysteria" in meta and node.get("hy2_password"):
|
||||
# 旧配置未设置 hy2 name 时,Clash API 可能不带 user
|
||||
if user == node["hy2_password"]:
|
||||
return True
|
||||
|
||||
tag = _connection_inbound_tag(conn)
|
||||
node_id = int(node["id"])
|
||||
expected_hy2 = hy2_inbound_tag(node_id)
|
||||
if tag == expected_hy2:
|
||||
return True
|
||||
if tag == "hysteria2-in" and single_node:
|
||||
return True
|
||||
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:
|
||||
return str(conn.get("id") or "")
|
||||
|
||||
@@ -185,6 +241,7 @@ def _sync_connections(
|
||||
connections: list[dict],
|
||||
nodes: list[dict],
|
||||
uuid_to_node: dict[str, int],
|
||||
log_active: set[str],
|
||||
) -> dict[str, tuple[int, int]]:
|
||||
"""同步连接缓存,断开连接时写入累计流量,返回各用户当前活跃会话流量。"""
|
||||
seen: set[str] = set()
|
||||
@@ -195,7 +252,9 @@ def _sync_connections(
|
||||
for conn in connections:
|
||||
matched_uuid = ""
|
||||
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"]
|
||||
break
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
nodes = list_nodes()
|
||||
uuid_to_node = {node["uuid"]: int(node["id"]) for node in nodes}
|
||||
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
|
||||
has_connections = len(connections) > 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
|
||||
up_speed, down_speed = _calc_speed(node_id, display_up, display_down)
|
||||
|
||||
matched = [c for c in connections if _match_connection(c, node)]
|
||||
if not matched and single_node and has_connections:
|
||||
matched = connections
|
||||
if not matched and (session_up + session_down) > 0:
|
||||
matched = [None] # 有活跃会话但 Clash 未返回连接详情
|
||||
matched = _connections_for_node(connections, node, nodes, log_active)
|
||||
if not matched and single_node and global_active:
|
||||
up_speed = global_up_speed
|
||||
down_speed = global_down_speed
|
||||
@@ -288,6 +371,7 @@ def collect_node_stats() -> dict:
|
||||
online = (
|
||||
len(matched) > 0
|
||||
or (session_up + session_down) > 0
|
||||
or uid in log_active
|
||||
or (up_speed + down_speed) > 512
|
||||
or (single_node and (global_active or has_connections))
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<section class="hero">
|
||||
<div>
|
||||
<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>
|
||||
<button id="addBtn" class="btn primary">+ 添加节点</button>
|
||||
</section>
|
||||
|
||||
+1
-1
@@ -127,7 +127,7 @@ ufw default allow outgoing
|
||||
ufw allow 22/tcp comment 'SSH'
|
||||
ufw allow 80/tcp comment 'HTTP-ACME-Panel'
|
||||
ufw allow 443/tcp comment 'Reality'
|
||||
ufw allow 8443/udp comment 'Hysteria2'
|
||||
ufw allow 8443:8499/udp comment 'Hysteria2-multi-node'
|
||||
ufw --force enable
|
||||
|
||||
log "部署 Nginx fallback 站点 ..."
|
||||
|
||||
+34
-29
@@ -54,49 +54,54 @@ def build_config(env: dict[str, str], nodes: list[dict]) -> dict:
|
||||
raise SystemExit(f".env 缺少 {key}")
|
||||
|
||||
vless_users = [{"uuid": n["uuid"], "flow": "xtls-rprx-vision"} for n in nodes]
|
||||
# name 与 VLESS uuid 一致,便于 v2ray/clash API 按用户统计流量
|
||||
hy2_users = [
|
||||
{"name": n["uuid"], "password": n["hy2_password"]} for n in nodes
|
||||
]
|
||||
clash_secret = env.get("CLASH_API_SECRET", "")
|
||||
hy2_base_port = 8443
|
||||
|
||||
config = {
|
||||
"log": {"level": "warn", "timestamp": True},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "vless",
|
||||
"tag": "vless-reality-in",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 443,
|
||||
"users": vless_users,
|
||||
"tls": {
|
||||
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,
|
||||
"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"]],
|
||||
"handshake": {
|
||||
"server": env["REALITY_SERVER_NAME"],
|
||||
"server_port": 443,
|
||||
},
|
||||
"private_key": env["REALITY_PRIVATE_KEY"],
|
||||
"short_id": [env["REALITY_SHORT_ID"]],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
for index, node in enumerate(nodes):
|
||||
inbounds.append(
|
||||
{
|
||||
"type": "hysteria2",
|
||||
"tag": "hysteria2-in",
|
||||
"tag": f"hy2-in-{node['id']}",
|
||||
"listen": "0.0.0.0",
|
||||
"listen_port": 8443,
|
||||
"users": hy2_users,
|
||||
"listen_port": hy2_base_port + index,
|
||||
"users": [
|
||||
{"name": node["uuid"], "password": node["hy2_password"]},
|
||||
],
|
||||
"tls": {
|
||||
"enabled": True,
|
||||
"server_name": env["DOMAIN"],
|
||||
"certificate_path": "/etc/sing-box/certs/fullchain.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"}],
|
||||
"route": {
|
||||
"rules": [{"ip_is_private": True, "action": "reject"}],
|
||||
|
||||
@@ -20,6 +20,9 @@ server {
|
||||
__PANEL_ALLOW__
|
||||
proxy_pass http://127.0.0.1:5080/;
|
||||
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 X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
Reference in New Issue
Block a user