#!/usr/bin/env bash # 用隔离的虚拟 frps 探测 webui4frpc 行为。 # # 全部端口与现网隔离(现网:7000/4434/7500/18082),不修改任何生产配置。 # 用法:scripts/probe-with-virtual-frps.sh set -euo pipefail LAB="${LAB:-/tmp/frplab}" FRP_VER="${FRP_VER:-0.61.1}" FRP_TGZ="frp_${FRP_VER}_linux_amd64" UI_ADDR="${UI_ADDR:-127.0.0.1:17500}" UI_USER="${UI_USER:-labadmin}" UI_PASS="${UI_PASS:-labpass123}" LAB_TOKEN="${LAB_TOKEN:-lab-token-$(date +%Y%m%d)}" # 现网 frpc 二进制(唯一允许从生产读取的东西,只读) SRC_FRPC="${SRC_FRPC:-/opt/webui4frpc/data/bin/frpc-${FRP_VER}/frpc}" BACKEND_PORT=17090 TCP_PORT=17091 UI_FORWARD_PORT=17092 cleanup() { echo "==> 清理" pkill -f "webui4frpc -addr ${UI_ADDR}" 2>/dev/null || true pkill -f "frplab/.*frpc" 2>/dev/null || true [[ -n "${FRPS_PID:-}" ]] && kill "${FRPS_PID}" 2>/dev/null || true [[ -n "${BACKEND_PID:-}" ]] && kill "${BACKEND_PID}" 2>/dev/null || true } trap cleanup EXIT mkdir -p "${LAB}"/{frps,probe,logs,ui/data} # ---------- 1. frps(虚拟服务端) ---------- if [[ ! -x "${LAB}/frpsd" ]]; then echo "==> 下载 frps ${FRP_VER}" curl -sL -o "${LAB}/frps-bin" \ "https://github.com/fatedier/frp/releases/download/v${FRP_VER}/${FRP_TGZ}.tar.gz" tar xzf "${LAB}/frps-bin" -C "${LAB}" cp "${LAB}/${FRP_TGZ}/frps" "${LAB}/frpsd" chmod +x "${LAB}/frpsd" fi cat > "${LAB}/frps/frps.toml" < 启动虚拟 frps (17000/17080/17443)" "${LAB}/frpsd" -c "${LAB}/frps/frps.toml" > "${LAB}/logs/frps-stdout.log" 2>&1 & FRPS_PID=$! sleep 3 ss -ltn | grep -E ':17000|:17443' || { echo "frps 未监听"; exit 1; } # ---------- 2. 后端服务 ---------- echo "==> 启动后端 http://127.0.0.1:${BACKEND_PORT}" ( cd "${LAB}" && python3 -m http.server "${BACKEND_PORT}" --bind 127.0.0.1 > logs/backend.log 2>&1 & ) BACKEND_PID=$! sleep 1 # ---------- 3. 裸 frpc 验证 frp 协议本身 ---------- cat > "${LAB}/probe/frpc-probe.json" < 裸 frpc 连接(验证 frp 协议通路)" "${LAB}/frpc" -c "${LAB}/probe/frpc-probe.json" > "${LAB}/logs/frpc.log" 2>&1 & sleep 4 grep -q 'login to server success' "${LAB}/logs/frpc.log" \ && echo " frpc 登录成功" || { echo " frpc 登录失败"; tail -5 "${LAB}/logs/frpc.log"; } printf " tcp ${TCP_PORT} -> "; curl -s -o /dev/null -w "%{http_code}\n" "http://127.0.0.1:${TCP_PORT}/" pkill -f 'frplab/frpc' 2>/dev/null || true # ---------- 4. webui4frpc 接管 ---------- echo "==> 启动 webui4frpc ${UI_ADDR}(独立 workdir)" mkdir -p "${LAB}/ui/data/bin/frpc-${FRP_VER}" cp "${SRC_FRPC}" "${LAB}/ui/data/bin/frpc-${FRP_VER}/frpc" chmod +x "${LAB}/ui/data/bin/frpc-${FRP_VER}/frpc" ( setsid /opt/webui4frpc/webui4frpc -addr "${UI_ADDR}" \ -user "${UI_USER}" -password "${UI_PASS}" \ -workdir "${LAB}/ui/data" > "${LAB}/logs/ui.log" 2>&1 < /dev/null & ) sleep 5 ss -ltn | grep -q "${UI_ADDR##*:}" || { echo "webui4frpc 未监听"; exit 1; } AUTH="${UI_USER}:${UI_PASS}" echo "==> 注入画布(注意:loopback 会被自动改写为 LAN 地址)" curl -s -u "${AUTH}" -X PUT "http://${UI_ADDR}/api/manager/canvas" \ -H 'Content-Type: application/json' \ -d "{\"remotes\":[{\"name\":\"lab-frps\",\"ip\":\"127.0.0.1\",\"port\":17000,\"token\":\"${LAB_TOKEN}\",\"enabled\":true}], \"locals\":[{\"name\":\"lab-backend\",\"ip\":\"127.0.0.1\",\"port\":${BACKEND_PORT}}], \"links\":[{\"local\":\"lab-backend\",\"remote\":\"lab-frps\",\"remotePort\":${UI_FORWARD_PORT}}]}" \ | python3 -m json.tool echo "==> binaryPath 必须先写入 settings,否则 worker 静默不启动" python3 - "${LAB}/ui/data/manager.db" "${LAB}/ui/data/bin/frpc-${FRP_VER}/frpc" <<'PY' import json, sqlite3, sys db, binary = sys.argv[1], sys.argv[2] c = sqlite3.connect(db) raw = c.execute("select value from settings where key='settings'").fetchone()[0] d = json.loads(raw); d["binaryPath"] = binary c.execute("update settings set value=? where key='settings'", (json.dumps(d),)) c.commit() print(" binaryPath 已写入:", binary) PY echo "==> 启动 forward(非 localOnly 需 owner claim,观察 status)" curl -s -u "${AUTH}" -X POST "http://${UI_ADDR}/api/manager/forwards/start" \ -H 'Content-Type: application/json' \ -d "{\"local\":\"lab-backend\",\"remote\":\"lab-frps\",\"remotePort\":${UI_FORWARD_PORT}}" echo sleep 5 echo "==> 观察结果" curl -s -u "${AUTH}" "http://${UI_ADDR}/api/manager/status" | python3 -c " import json,sys d=json.load(sys.stdin) print(' forwards:', [(f['local'], f['active']) for f in d.get('forwards',[])]) print(' profiles:', [(p['name'], p['process']['state'], p.get('connState')) for p in d.get('profiles',[])]) " echo " binaryPath: $(curl -s -u "${AUTH}" "http://${UI_ADDR}/api/manager/binary/status")" echo " configs/: $(ls "${LAB}/ui/data/configs/" 2>/dev/null | tr '\n' ' ')" echo " workers: $(pgrep -af "${LAB}/ui/data" | wc -l) 个" echo echo "日志: ${LAB}/logs/" echo "复现结论见 docs/how-it-works.md"