feat: 合并边缘节点运维并完善 Surge 配置安全
迁入通用节点部署工具、离线交付页生成器和共用技能,隔离真实配置与交付产物。统一 Codex 与 Claude Code 开发约定,补充节点迁移与运维文档。 完善 FINAL、策略组引用、输入校验、Surge 字段编码与 AI DNS 例外保护,增加隔离测试和订阅验证工具。 验证:全量类型检查、44 项单测、前端构建、脚本语法、技能入口与凭据隔离检查均通过。
This commit is contained in:
105
.claude/skills/edge-node-multi-protocol/scripts/13-singbox-config.py
Executable file
105
.claude/skills/edge-node-multi-protocol/scripts/13-singbox-config.py
Executable 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)))
|
||||
Reference in New Issue
Block a user