迁入通用节点部署工具、离线交付页生成器和共用技能,隔离真实配置与交付产物。统一 Codex 与 Claude Code 开发约定,补充节点迁移与运维文档。 完善 FINAL、策略组引用、输入校验、Surge 字段编码与 AI DNS 例外保护,增加隔离测试和订阅验证工具。 验证:全量类型检查、44 项单测、前端构建、脚本语法、技能入口与凭据隔离检查均通过。
106 lines
4.2 KiB
Python
Executable File
106 lines
4.2 KiB
Python
Executable File
#!/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)))
|