feat: 合并边缘节点运维并完善 Surge 配置安全

迁入通用节点部署工具、离线交付页生成器和共用技能,隔离真实配置与交付产物。统一 Codex 与 Claude Code 开发约定,补充节点迁移与运维文档。

完善 FINAL、策略组引用、输入校验、Surge 字段编码与 AI DNS 例外保护,增加隔离测试和订阅验证工具。

验证:全量类型检查、44 项单测、前端构建、脚本语法、技能入口与凭据隔离检查均通过。
This commit is contained in:
2026-10-03 23:19:59 +08:00
parent 7358008936
commit 5326c572a2
93 changed files with 6780 additions and 320 deletions

View File

@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Runs ON the box. Emits /etc/sing-box/config.json for the QUIC plane.
Which inbounds get built is driven purely by which port variables are set, so the
same script serves a Hysteria2-only node, a TUIC-only node, or both -- no editing
required to add or drop a protocol.
Why a second daemon at all: the TCP inbounds (VMess/Trojan/Shadowsocks) hide
behind nginx:443 as WebSocket paths. Hysteria2 and TUIC are QUIC over UDP -- they
terminate TLS themselves and cannot be reverse-proxied, so each needs a real
public UDP port. Xray-core (what 3x-ui drives) implements neither, hence sing-box
running alongside 3x-ui. Nothing here touches 3x-ui, nginx, or the existing
inbounds: separate service, separate ports, separate config file.
WARP egress reuses the cloudflare-warp SOCKS5 proxy the Xray plane already uses,
routed by inbound tag -- one WARP install serves both planes.
Env in: COVER, CERT_DIR, WARP_SOCKS_PORT, NODE_PASSWORD, XUI_UUID,
PORT_HY2_DIRECT / PORT_HY2_WARP / HY2_OBFS_PASSWORD / HY2_UP_MBPS / HY2_DOWN_MBPS,
PORT_TUIC_DIRECT / PORT_TUIC_WARP / TUIC_CC
Unset port pairs are simply skipped.
"""
import json, os, sys
COVER = os.environ["COVER"]
CERT_DIR = os.environ.get("CERT_DIR", "/etc/nginx/ssl/" + COVER.split(".", 1)[1])
WARP_PORT = int(os.environ.get("WARP_SOCKS_PORT", "40000"))
OUT = os.environ.get("SINGBOX_CONFIG", "/etc/sing-box/config.json")
def tls():
return {"enabled": True, "server_name": COVER, "alpn": ["h3"],
"certificate_path": os.path.join(CERT_DIR, "fullchain.pem"),
"key_path": os.path.join(CERT_DIR, "privkey.pem")}
def port(name):
v = os.environ.get(name, "").strip()
return int(v) if v else None
inbounds = []
# --- Hysteria2 -------------------------------------------------------------
# Brutal congestion control needs a DECLARED bandwidth; it ignores loss as a
# congestion signal, so an over-stated figure causes a loss storm rather than
# speed. up/down are the server's view of the link.
up = int(os.environ.get("HY2_UP_MBPS", "500"))
down = int(os.environ.get("HY2_DOWN_MBPS", "500"))
obfs_pw = os.environ.get("HY2_OBFS_PASSWORD", "").strip()
for tag, pvar in (("hy2-direct", "PORT_HY2_DIRECT"), ("hy2-warp", "PORT_HY2_WARP")):
p = port(pvar)
if not p:
continue
ib = {"type": "hysteria2", "tag": tag, "listen": "::", "listen_port": p,
"up_mbps": up, "down_mbps": down,
"users": [{"name": "node-main", "password": os.environ["NODE_PASSWORD"]}],
"tls": tls()}
if obfs_pw:
ib["obfs"] = {"type": "salamander", "password": obfs_pw}
inbounds.append(ib)
# --- TUIC v5 --------------------------------------------------------------
tuic_cc = os.environ.get("TUIC_CC", "bbr")
for tag, pvar in (("tuic-direct", "PORT_TUIC_DIRECT"), ("tuic-warp", "PORT_TUIC_WARP")):
p = port(pvar)
if not p:
continue
inbounds.append({
"type": "tuic", "tag": tag, "listen": "::", "listen_port": p,
"users": [{"name": "node-main", "uuid": os.environ["XUI_UUID"],
"password": os.environ["NODE_PASSWORD"]}],
"congestion_control": tuic_cc, "auth_timeout": "3s",
"zero_rtt_handshake": False, "heartbeat": "10s", "tls": tls()})
if not inbounds:
sys.exit("ERROR: no QUIC ports configured (set PORT_HY2_* and/or PORT_TUIC_*)")
warp_tags = [i["tag"] for i in inbounds if i["tag"].endswith("-warp")]
direct_tags = [i["tag"] for i in inbounds if i["tag"].endswith("-direct")]
rules = []
if warp_tags:
rules.append({"inbound": warp_tags, "outbound": "warp"})
if direct_tags:
rules.append({"inbound": direct_tags, "outbound": "direct"})
rules.append({"protocol": "bittorrent", "outbound": "block"})
cfg = {
"log": {"level": "warn", "timestamp": True},
"inbounds": inbounds,
"outbounds": [
{"type": "direct", "tag": "direct"},
{"type": "socks", "tag": "warp", "server": "127.0.0.1",
"server_port": WARP_PORT, "version": "5"},
{"type": "block", "tag": "block"},
],
"route": {"rules": rules, "final": "direct"},
}
os.makedirs(os.path.dirname(OUT), exist_ok=True)
with open(OUT, "w") as f:
json.dump(cfg, f, indent=2)
os.chmod(OUT, 0o600)
print("wrote %s with %d inbound(s): %s" % (OUT, len(inbounds), ", ".join(i["tag"] for i in inbounds)))