feat: 合并边缘节点运维并完善 Surge 配置安全
迁入通用节点部署工具、离线交付页生成器和共用技能,隔离真实配置与交付产物。统一 Codex 与 Claude Code 开发约定,补充节点迁移与运维文档。 完善 FINAL、策略组引用、输入校验、Surge 字段编码与 AI DNS 例外保护,增加隔离测试和订阅验证工具。 验证:全量类型检查、44 项单测、前端构建、脚本语法、技能入口与凭据隔离检查均通过。
This commit is contained in:
142
.claude/skills/edge-node-multi-protocol/scripts/12-smoke-test.sh
Executable file
142
.claude/skills/edge-node-multi-protocol/scripts/12-smoke-test.sh
Executable file
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
# End-to-end smoke test for all 6 inbounds (vmess/trojan/shadowsocks x direct/warp),
|
||||
# plus the landing page, both panels, and the anti-scan default-444 catch-all.
|
||||
# Run from anywhere with DNS resolving to the box.
|
||||
. "$(dirname "$0")/_lib.sh"
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
COVER="$SUB_COVER.$DOMAIN_ROOT"
|
||||
|
||||
echo "== credential integrity (catches the post-upgrade client wipe) =="
|
||||
# An upgrade that runs `x-ui migrate` rebuilds client_inbounds from each inbound's
|
||||
# settings.clients JSON, deleting the old rows first. If settings.clients is empty
|
||||
# the node keeps answering TLS + WS-101 while silently authenticating nobody, so the
|
||||
# transport checks below all pass on a completely dead node. Assert the credentials
|
||||
# are actually present, in the DB and in the generated xray config. See fix #5 in
|
||||
# 08-xui-bootstrap.py; retrofit with 08b-embed-settings-clients.py.
|
||||
run 'python3 - <<PY
|
||||
import json, sqlite3, sys
|
||||
PROXY = ("vmess", "vless", "trojan", "shadowsocks")
|
||||
bad = []
|
||||
con = sqlite3.connect("/etc/x-ui/x-ui.db")
|
||||
rows = con.execute("SELECT port,protocol,settings FROM inbounds ORDER BY port").fetchall()
|
||||
proxy_rows = [r for r in rows if r[1] in PROXY]
|
||||
for port, proto, s in proxy_rows:
|
||||
try:
|
||||
cl = (json.loads(s) or {}).get("clients")
|
||||
except (TypeError, ValueError):
|
||||
cl = None
|
||||
if not isinstance(cl, list) or not cl:
|
||||
bad.append("inbound :%s (%s) settings.clients is EMPTY" % (port, proto))
|
||||
links = con.execute("SELECT count(*) FROM client_inbounds").fetchone()[0]
|
||||
if links < len(proxy_rows):
|
||||
bad.append("client_inbounds has %d row(s), expected >= %d" % (links, len(proxy_rows)))
|
||||
cfg = json.load(open("/usr/local/x-ui/bin/config.json"))
|
||||
for i in cfg.get("inbounds", []):
|
||||
if i.get("protocol") not in PROXY:
|
||||
continue
|
||||
cl = (i.get("settings") or {}).get("clients")
|
||||
if not isinstance(cl, list) or not cl:
|
||||
bad.append("generated config: inbound :%s (%s) has NO clients" % (i.get("port"), i.get("protocol")))
|
||||
if bad:
|
||||
print("FAIL -- credentials missing; every inbound will reject all traffic:")
|
||||
for b in bad:
|
||||
print(" -", b)
|
||||
print(" fix: run scripts/08b-embed-settings-clients.py on the box")
|
||||
sys.exit(1)
|
||||
print("OK -- %d inbounds have embedded clients, %d client_inbounds links, config has credentials"
|
||||
% (len(proxy_rows), links))
|
||||
PY' || echo "!! credential check FAILED (see above) -- transport checks below may still pass on a dead node"
|
||||
|
||||
echo "== cover WS upgrade, all 6 paths (expect 101 each) =="
|
||||
# curl reports the upgrade status via -w before it ever exits, but the connection stays
|
||||
# open as a live WS tunnel afterwards, so curl commonly hits --max-time and returns a
|
||||
# non-zero exit even on a successful 101 -- `|| true` so one check's exit code can't
|
||||
# abort the rest of the script under `set -e`.
|
||||
for P in "$WS_PATH_VMESS_DIRECT" "$WS_PATH_VMESS_WARP" "$WS_PATH_TROJAN_DIRECT" "$WS_PATH_TROJAN_WARP" \
|
||||
"$WS_PATH_SS_DIRECT" "$WS_PATH_SS_WARP"; do
|
||||
curl -sk --http1.1 --max-time 20 -o /dev/null -w "$P -> %{http_code}\n" \
|
||||
-H "Connection: Upgrade" -H "Upgrade: websocket" \
|
||||
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" -H "Sec-WebSocket-Version: 13" \
|
||||
"https://$COVER$P" || true
|
||||
done
|
||||
|
||||
echo "== landing + panels (expect 200) =="
|
||||
curl -sk --max-time 10 -o /dev/null -w "landing -> %{http_code}\n" "https://$DOMAIN_ROOT/" || true
|
||||
curl -sk --max-time 10 -o /dev/null -w "3x-ui -> %{http_code}\n" "https://$SUB_XUI.$DOMAIN_ROOT$XUI_PATH" || true
|
||||
# aaPanel's anti-scan check needs a UA that actually looks like a real browser -- a bare
|
||||
# "Mozilla/5.0 (Macintosh)" gets judged as a scanner and served a fake 404 (see
|
||||
# references/pitfalls.md). Don't "fix" this by loosening CDN_PROXY or re-checking the
|
||||
# domain binding; check the UA string first, every time.
|
||||
curl -sk --max-time 10 -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15" \
|
||||
-o /dev/null -w "aaPanel -> %{http_code}\n" "https://$SUB_PANEL.$DOMAIN_ROOT$AAPANEL_ENTRANCE" || true
|
||||
|
||||
echo "== unmatched host (expect 000/444, connection dropped) =="
|
||||
curl -sk --max-time 8 -o /dev/null -w "random -> %{http_code}\n" "https://nope-$RANDOM.$DOMAIN_ROOT/" || true
|
||||
|
||||
echo "== e2e egress via temp xray client, all 6 outbounds (direct vs WARP per protocol) =="
|
||||
# node-main carries one uuid (vmess) + one shared password (trojan+ss).
|
||||
python3 - "$COVER" "$XUI_UUID" "$NODE_PASSWORD" "${SS_METHOD:-chacha20-poly1305}" \
|
||||
"$WS_PATH_VMESS_DIRECT" "$WS_PATH_VMESS_WARP" "$WS_PATH_TROJAN_DIRECT" "$WS_PATH_TROJAN_WARP" \
|
||||
"$WS_PATH_SS_DIRECT" "$WS_PATH_SS_WARP" <<'PYEOF'
|
||||
import json, sys
|
||||
(cover, uuid, node_pw, ss_method,
|
||||
vd, vw, td, tw, sd, sw) = sys.argv[1:11]
|
||||
trojan_pw = ss_pw = node_pw # single shared credential
|
||||
|
||||
def ws(path):
|
||||
return {"network": "ws", "security": "tls",
|
||||
"tlsSettings": {"serverName": cover},
|
||||
"wsSettings": {"path": path, "headers": {"Host": cover}}}
|
||||
|
||||
outbounds = [
|
||||
{"tag": "vmess-d", "protocol": "vmess",
|
||||
"settings": {"vnext": [{"address": cover, "port": 443, "users": [{"id": uuid, "security": "auto"}]}]},
|
||||
"streamSettings": ws(vd)},
|
||||
{"tag": "vmess-w", "protocol": "vmess",
|
||||
"settings": {"vnext": [{"address": cover, "port": 443, "users": [{"id": uuid, "security": "auto"}]}]},
|
||||
"streamSettings": ws(vw)},
|
||||
{"tag": "trojan-d", "protocol": "trojan",
|
||||
"settings": {"servers": [{"address": cover, "port": 443, "password": trojan_pw}]},
|
||||
"streamSettings": ws(td)},
|
||||
{"tag": "trojan-w", "protocol": "trojan",
|
||||
"settings": {"servers": [{"address": cover, "port": 443, "password": trojan_pw}]},
|
||||
"streamSettings": ws(tw)},
|
||||
{"tag": "ss-d", "protocol": "shadowsocks",
|
||||
"settings": {"servers": [{"address": cover, "port": 443, "method": ss_method, "password": ss_pw}]},
|
||||
"streamSettings": ws(sd)},
|
||||
{"tag": "ss-w", "protocol": "shadowsocks",
|
||||
"settings": {"servers": [{"address": cover, "port": 443, "method": ss_method, "password": ss_pw}]},
|
||||
"streamSettings": ws(sw)},
|
||||
]
|
||||
inbounds = []
|
||||
rules = []
|
||||
for i, tag in enumerate(("vmess-d", "vmess-w", "trojan-d", "trojan-w", "ss-d", "ss-w")):
|
||||
port = 10801 + i
|
||||
inbounds.append({"port": port, "listen": "127.0.0.1", "protocol": "socks",
|
||||
"tag": "in-%s" % tag, "settings": {"udp": True}})
|
||||
rules.append({"type": "field", "inboundTag": ["in-%s" % tag], "outboundTag": tag})
|
||||
|
||||
cfg = {"inbounds": inbounds, "outbounds": outbounds, "routing": {"rules": rules}}
|
||||
json.dump(cfg, open("/tmp/_smoke_tc.json", "w"))
|
||||
print("local socks ports: vmess-d=10801 vmess-w=10802 trojan-d=10803 trojan-w=10804 ss-d=10805 ss-w=10806")
|
||||
PYEOF
|
||||
|
||||
push /tmp/_smoke_tc.json /tmp/_smoke_tc.json
|
||||
run '
|
||||
cd /usr/local/x-ui/bin
|
||||
XRAY=$(ls xray-linux-* 2>/dev/null | head -1); [ -n "$XRAY" ] || XRAY=$(ls xray* 2>/dev/null | head -1)
|
||||
./$XRAY -c /tmp/_smoke_tc.json >/tmp/_smoke_tc.log 2>&1 &
|
||||
XPID=$!; sleep 4
|
||||
for pair in "vmess-direct:10801" "vmess-warp:10802" "trojan-direct:10803" "trojan-warp:10804" "ss-direct:10805" "ss-warp:10806"; do
|
||||
name="${pair%%:*}"; port="${pair##*:}"
|
||||
printf "%-14s " "$name"
|
||||
curl -s -x socks5h://127.0.0.1:$port --max-time 15 https://1.1.1.1/cdn-cgi/trace | grep -E "ip=|warp=" | tr "\n" " "
|
||||
echo
|
||||
done
|
||||
kill $XPID 2>/dev/null
|
||||
rm -f /tmp/_smoke_tc.json /tmp/_smoke_tc.log
|
||||
'
|
||||
rm -f /tmp/_smoke_tc.json
|
||||
|
||||
echo "Expected: *-direct -> ip=$SERVER_IP warp=off ; *-warp -> ip=<a Cloudflare address> warp=on"
|
||||
Reference in New Issue
Block a user