#!/usr/bin/env bash # End-to-end smoke test (no aaPanel): all 6 inbounds (vmess/trojan/ss x direct/warp), # landing page, 3x-ui panel, and the anti-scan default-444 catch-all. . "$(dirname "$0")/_lib.sh" COVER="$SUB_COVER.$DOMAIN_ROOT" echo "== credential integrity (catches the post-upgrade client wipe) ==" # `x-ui migrate` (run by install.sh/update.sh on every upgrade) 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 # authenticating nobody -- so the transport checks below pass on a dead node. # See fix #5 in 08-xui-bootstrap.py; retrofit with 08b-embed-settings-clients.py. run 'python3 - <= %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) ==" 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 + 3x-ui panel (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 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) ==" 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 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= warp=on"