diff --git a/.gitignore b/.gitignore index 5f827da..2dc6452 100644 --- a/.gitignore +++ b/.gitignore @@ -23,11 +23,10 @@ data/ # docker demo staging (frp binaries) frp-bin/ -deploy/frp-bin/ -# demo runtime data + compiled artifacts (regenerable) -deploy/data/ -deploy/artifacts/ +# local testing scripts, docker demo env, and screenshots (not for repo) +deploy/ +gui-test-screenshots/ # vendored deps (regenerable; keep out of git) vendor/ diff --git a/README.md b/README.md index 14a6aa0..58c10b3 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ rm -rf internal/httpapi/dist && cp -r web/dist internal/httpapi/dist 一键全量构建: ```bash -cd web && npm run build && cd .. && cp -r web/dist/* internal/httpapi/dist/ && go build -o deploy/artifacts/webui4frpc ./cmd/webui4frpc +cd web && npm run build && cd .. && cp -r web/dist/* internal/httpapi/dist/ && go build -o webui4frpc ./cmd/webui4frpc ``` ## 运行 @@ -173,31 +173,6 @@ webui4frpc(单二进制,无 frp 依赖,前端 go:embed) 集群页展示:环拓扑(self/leader 标记)、令牌轮次、节点负载、待办任务、活跃转发拓扑(含 owner 归属)、增量事件日志。 -## 集群演示 - -```bash -# 构建 -go build -o deploy/artifacts/webui4frpc ./cmd/webui4frpc - -# 拉起 4 节点集群 + 1 孤立节点(Docker) -cd deploy && bash run-cluster.sh - -# 运行转发测试(提交 4 条转发 → 验证分发 → 撤销) -bash test-forward.sh -``` - -节点端口映射: - -| 节点 | 端口 | 角色 | -|---|---|---| -| node-a | 7501 | 集群 leader(seed,有 frpc 缓存) | -| node-b | 7502 | 集群成员 | -| node-c | 7503 | 集群成员 | -| node-d | 7504 | 集群成员 | -| node-e | 7505 | 孤立节点(standalone,未加入集群) | - -所有节点 `-user=admin -password=admin123`。 - ## 测试 ```bash diff --git a/deploy/Dockerfile b/deploy/Dockerfile deleted file mode 100644 index 087d606..0000000 --- a/deploy/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -# Build the Go binary with embedded web UI + demo backend. -FROM golang:1.25.0 AS build -WORKDIR /src -COPY go.mod go.sum vendor ./ -COPY cmd ./cmd -COPY internal ./internal -# Vendored deps => fully offline build. -RUN CGO_ENABLED=0 go build -mod=vendor -trimpath -ldflags="-s -w" -o /out/webui4frpc ./cmd/webui4frpc - -# Static demo backend (tiny HTTP server). -COPY deploy/demo-backend ./demo-backend -RUN CGO_ENABLED=0 go build -mod=vendor -trimpath -ldflags="-s -w" -o /out/backend ./demo-backend - -# Runtime image: no frp binaries baked in (mounted via compose volume to keep -# layers small and build fast); static Go only. -FROM debian:bookworm-slim -WORKDIR /app -COPY --from=build /out/webui4frpc /app/webui4frpc -COPY --from=build /out/backend /app/backend -EXPOSE 7500 7000 7400 8080 -ENTRYPOINT ["/app/webui4frpc"] diff --git a/deploy/README.md b/deploy/README.md deleted file mode 100644 index 1b5d6f8..0000000 --- a/deploy/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# webui4frpc Docker 集群演示 - -一键起一套 **frps + backend + 3×webui4frpc 节点**(node-a 种子,node-b/c 通过 `-peer` 加入并自动从邻居拉取 frpc 二进制)。 - -## 前置 - -- Docker 可用(无需 build:用宿主编译产物挂载进容器) -- 宿主机有 `/home/jianf/Program/frp/bin/{frpc,frps}`(或设置 `FRP=` 指向别的目录) - -## 启动/停止 - -```bash -cd deploy -go build -o artifacts/webui4frpc ../cmd/webui4frpc # 首次/改代码后重建 -go build -o artifacts/backend ./demo-backend -bash run-cluster.sh # 启动集群 -bash run-cluster.sh stop # 停止 -``` - -## 拓扑 - -``` - ┌──────────┐ ┌───────────────────────┐ - │ w4f-frps │◄───│ node-a (7501, 种子) │ - tunnel: │ 7000/7080/18081 ├── node-b (7502, peer a)│ - http://127.0.0.1:18081 └──────────┘ └── node-c (7503, peer b)│ - ▼ └───────────────────────┘ - w4f-backend (8080) ← frpc 转发 -``` - -## 演示点 - -- **M6 二进制分发**:node-a 预置 `bin/frpc-0.70.1/frpc`;node-b/c 无 frpc,入网后自动向邻居拉取并落盘缓存 -- **M3 状态**:node-a 配置 `adminPort`,状态页显示真实 per-proxy 状态(running) -- **隧道**:node-a 画布 local=backend:8080 × remote=frps1,frpc worker 启动后经 frps:18081 访问 backend - -## 验证 - -```bash -# 二进制分发 -curl -s -u admin:admin123 http://127.0.0.1:7502/api/manager/cluster/nodes -# 隧道 -curl http://127.0.0.1:18081/ # -> hello from w4f demo backend -# 代理状态 -curl -s -u admin:admin123 http://127.0.0.1:17401/api/status -``` diff --git a/deploy/compose.yaml b/deploy/compose.yaml deleted file mode 100644 index 232f965..0000000 --- a/deploy/compose.yaml +++ /dev/null @@ -1,114 +0,0 @@ -services: - frps: - image: w4f-demo:latest - container_name: w4f-frps - command: ["/app/frp/frps", "-c", "/app/frps.toml"] - volumes: - - ./frps.toml:/app/frps.toml:ro - - /home/jianf/Program/frp/bin:/app/frp:ro - ports: - - "7000:7000" # frps control - - "7080:7080" # vhostHTTPPort - networks: - - w4f - - frps2: - image: w4f-demo:latest - container_name: w4f-frps2 - command: ["/app/frp/frps", "-c", "/app/frps.toml"] - volumes: - - ./frps2.toml:/app/frps.toml:ro - - /home/jianf/Program/frp/bin:/app/frp:ro - ports: - - "7001:7001" # frps control - - "7081:7081" # vhostHTTPPort - networks: - - w4f - - frps3: - image: w4f-demo:latest - container_name: w4f-frps3 - command: ["/app/frp/frps", "-c", "/app/frps.toml"] - volumes: - - ./frps3.toml:/app/frps.toml:ro - - /home/jianf/Program/frp/bin:/app/frp:ro - ports: - - "7002:7002" # frps control - - "7082:7082" # vhostHTTPPort - networks: - - w4f - - backend: - image: w4f-demo:latest - container_name: w4f-backend - command: ["/app/backend"] - ports: - - "8080:8080" - networks: - - w4f - - node-a: - image: w4f-demo:latest - container_name: w4f-node-a - command: - - "-addr=0.0.0.0:7500" - - "-user=admin" - - "-password=admin123" - - "-workdir=/data" - - "-frpc=/app/frp/frpc" - volumes: - - ./node-a:/data - - /home/jianf/Program/frp/bin:/app/frp:ro - ports: - - "7501:7500" - - "17401:7400" - depends_on: - - frps - networks: - - w4f - - node-b: - image: w4f-demo:latest - container_name: w4f-node-b - command: - - "-addr=0.0.0.0:7500" - - "-user=admin" - - "-password=admin123" - - "-workdir=/data" - - "-frpc=/app/frp/frpc" - - "-peer=node-a:7500" - volumes: - - ./node-b:/data - - /home/jianf/Program/frp/bin:/app/frp:ro - ports: - - "7502:7500" - depends_on: - - frps - - node-a - networks: - - w4f - - node-c: - image: w4f-demo:latest - container_name: w4f-node-c - command: - - "-addr=0.0.0.0:7500" - - "-user=admin" - - "-password=admin123" - - "-workdir=/data" - - "-frpc=/app/frp/frpc" - - "-peer=node-b:7500" - volumes: - - ./node-c:/data - - /home/jianf/Program/frp/bin:/app/frp:ro - ports: - - "7503:7500" - depends_on: - - frps - - node-b - networks: - - w4f - -networks: - w4f: - driver: bridge diff --git a/deploy/demo-backend/main.go b/deploy/demo-backend/main.go deleted file mode 100644 index 1e9dcb0..0000000 --- a/deploy/demo-backend/main.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import ( - "fmt" - "net/http" -) - -func main() { - http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }) - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "hello from w4f demo backend (path=%s)", r.URL.Path) - }) - _ = http.ListenAndServe("0.0.0.0:8080", nil) -} diff --git a/deploy/frps.toml b/deploy/frps.toml deleted file mode 100644 index 177d953..0000000 --- a/deploy/frps.toml +++ /dev/null @@ -1,16 +0,0 @@ -# frps demo config for the cluster showcase. -bindAddr = "0.0.0.0" -bindPort = 7000 - -auth.method = "token" -auth.token = "demo-token" - -# HTTP virtual host port (for http proxies) -vhostHTTPPort = 7080 - -webServer.addr = "0.0.0.0" -webServer.port = 7500 - -log.to = "/app/frps.log" -log.level = "info" -log.maxDays = 3 diff --git a/deploy/frps2.toml b/deploy/frps2.toml deleted file mode 100644 index fbf4431..0000000 --- a/deploy/frps2.toml +++ /dev/null @@ -1,16 +0,0 @@ -# Mock frps #2 for forwarding dispatch/enablement testing. -# Same auth token as the primary frps so a single frpc worker config works -# against any of them; distinct bindPort / vhostHTTPPort so multiple can run -# on the same docker network without clashing. -bindAddr = "0.0.0.0" -bindPort = 7001 - -auth.method = "token" -auth.token = "demo-token" - -# HTTP virtual host port (for http proxies) -vhostHTTPPort = 7081 - -log.to = "/app/frps.log" -log.level = "info" -log.maxDays = 3 diff --git a/deploy/frps3.toml b/deploy/frps3.toml deleted file mode 100644 index 75c47ee..0000000 --- a/deploy/frps3.toml +++ /dev/null @@ -1,16 +0,0 @@ -# Mock frps #3 for forwarding dispatch/enablement testing. -# Same auth token as the primary frps so a single frpc worker config works -# against any of them; distinct bindPort / vhostHTTPPort so multiple can run -# on the same docker network without clashing. -bindAddr = "0.0.0.0" -bindPort = 7002 - -auth.method = "token" -auth.token = "demo-token" - -# HTTP virtual host port (for http proxies) -vhostHTTPPort = 7082 - -log.to = "/app/frps.log" -log.level = "info" -log.maxDays = 3 diff --git a/deploy/run-cluster.sh b/deploy/run-cluster.sh deleted file mode 100755 index e4deba3..0000000 --- a/deploy/run-cluster.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env bash -# w4f demo cluster: 4-node ring (a=leader, b/c/d join) + 1 isolated standalone (e). -set -euo pipefail -cd "$(dirname "$0")" - -FRP=${FRP:-/home/jianf/Program/frp/bin} -NET=w4f-demo -VER=0.70.1 - -docker network inspect $NET >/dev/null 2>&1 || docker network create $NET - -# clean + seed node-a cache with real frpc binary -for d in node-a node-b node-c node-d node-e; do rm -rf data/$d 2>/dev/null || true; mkdir -p data/$d; done -mkdir -p data/node-a/bin/frpc-$VER -[ -x data/node-a/bin/frpc-$VER/frpc ] || cp $FRP/frpc data/node-a/bin/frpc-$VER/frpc -chmod +x data/node-a/bin/frpc-$VER/frpc - -echo "== starting backend ==" -docker run -d --name w4f-backend --user $(id -u):$(id -g) --network $NET --network-alias backend -p 18080:8080 -v $PWD/artifacts/backend:/app/backend:ro --restart unless-stopped --entrypoint /app/backend debian:bookworm-slim - -echo "== starting frps ==" -docker run -d --name w4f-frps --user $(id -u):$(id -g) --network $NET --network-alias frps -p 7000:7000 -p 7080:7080 -p 18081:18081 -v $FRP/frps:/app/frps:ro -v $PWD/frps.toml:/app/frps.toml:ro --restart unless-stopped --entrypoint /app/frps debian:bookworm-slim -c /app/frps.toml - -echo "== starting mock frps2 (bindPort 7001) ==" -docker run -d --name w4f-frps2 --user $(id -u):$(id -g) --network $NET --network-alias frps2 -p 7001:7001 -p 7081:7081 -v $FRP/frps:/app/frps:ro -v $PWD/frps2.toml:/app/frps.toml:ro --restart unless-stopped --entrypoint /app/frps debian:bookworm-slim -c /app/frps.toml - -echo "== starting mock frps3 (bindPort 7002) ==" -docker run -d --name w4f-frps3 --user $(id -u):$(id -g) --network $NET --network-alias frps3 -p 7002:7002 -p 7082:7082 -v $FRP/frps:/app/frps:ro -v $PWD/frps3.toml:/app/frps.toml:ro --restart unless-stopped --entrypoint /app/frps debian:bookworm-slim -c /app/frps.toml - -echo "== starting node-a (seed, has frpc cache) ==" -docker run -d --name w4f-node-a --user $(id -u):$(id -g) --network $NET --network-alias node-a -e W4F_HOST=node-a -p 7501:7500 -p 17401:7400 -v $PWD/artifacts/webui4frpc:/app/webui4frpc:ro -v $PWD/data/node-a:/data -v $FRP:/app/frp:ro --restart unless-stopped --entrypoint /app/webui4frpc debian:bookworm-slim -addr=0.0.0.0:7500 -user=admin -password=admin123 -workdir=/data -frpc=/app/frp/frpc - -# Wait for node-a to be ready and fetch its admission key. Nodes b/c/d join -# via node-a, so they need node-a's nodeKey (-join-key) to pass the sponsor -# verification in handleClusterJoin (403 on mismatch/empty). Node-e is isolated. -echo "== fetching node-a join key ==" -KEY="" -for _ in $(seq 1 15); do - KEY=$(curl -fsS -u admin:admin123 http://localhost:7501/api/manager/cluster/ring 2>/dev/null \ - | python3 -c "import sys,json; print(json.load(sys.stdin).get('nodeKey',''))" 2>/dev/null || true) - [ -n "$KEY" ] && break - sleep 1 -done -if [ -z "$KEY" ]; then echo "FATAL: could not fetch node-a join key"; docker logs w4f-node-a 2>&1 | tail 20; exit 1; fi -echo " node-a join key: $KEY" - -echo "== starting node-b (no frpc; joins via node-a) ==" -docker run -d --name w4f-node-b --user $(id -u):$(id -g) --network $NET --network-alias node-b -e W4F_HOST=node-b -p 7502:7500 -v $PWD/artifacts/webui4frpc:/app/webui4frpc:ro -v $PWD/data/node-b:/data -v $FRP:/app/frp:ro --restart unless-stopped --entrypoint /app/webui4frpc debian:bookworm-slim -addr=0.0.0.0:7500 -user=admin -password=admin123 -workdir=/data -frpc=/app/frp/frpc -peer=node-a:7500 -join-key=$KEY - -echo "== starting node-c (no frpc; joins via node-a) ==" -docker run -d --name w4f-node-c --user $(id -u):$(id -g) --network $NET --network-alias node-c -e W4F_HOST=node-c -p 7503:7500 -v $PWD/artifacts/webui4frpc:/app/webui4frpc:ro -v $PWD/data/node-c:/data -v $FRP:/app/frp:ro --restart unless-stopped --entrypoint /app/webui4frpc debian:bookworm-slim -addr=0.0.0.0:7500 -user=admin -password=admin123 -workdir=/data -frpc=/app/frp/frpc -peer=node-a:7500 -join-key=$KEY - -echo "== starting node-d (no frpc; joins via node-a) ==" -docker run -d --name w4f-node-d --user $(id -u):$(id -g) --network $NET --network-alias node-d -e W4F_HOST=node-d -p 7504:7500 -v $PWD/artifacts/webui4frpc:/app/webui4frpc:ro -v $PWD/data/node-d:/data -v $FRP:/app/frp:ro --restart unless-stopped --entrypoint /app/webui4frpc debian:bookworm-slim -addr=0.0.0.0:7500 -user=admin -password=admin123 -workdir=/data -frpc=/app/frp/frpc -peer=node-a:7500 -join-key=$KEY - -echo "== starting node-e (isolated standalone, not in cluster) ==" -docker run -d --name w4f-node-e --user $(id -u):$(id -g) --network $NET --network-alias node-e -e W4F_HOST=node-e -p 7505:7500 -v $PWD/artifacts/webui4frpc:/app/webui4frpc:ro -v $PWD/data/node-e:/data -v $FRP:/app/frp:ro --restart unless-stopped --entrypoint /app/webui4frpc debian:bookworm-slim -addr=0.0.0.0:7500 -user=admin -password=admin123 -workdir=/data -frpc=/app/frp/frpc - -echo "== cluster up ==" -docker ps --filter "name=w4f-" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" \ No newline at end of file diff --git a/deploy/start-mock-frps.sh b/deploy/start-mock-frps.sh deleted file mode 100755 index 5ba2a02..0000000 --- a/deploy/start-mock-frps.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -# Start (or restart) just the mock frps2/frps3 containers on the demo network, -# leaving the rest of the cluster untouched. Used by test-forward.sh and when -# the cluster is already running but the mock frps nodes are not. -set -euo pipefail -cd "$(dirname "$0")" - -FRP=${FRP:-/home/jianf/Program/frp/bin} -NET=${W4F_NET:-w4f-demo} - -# detect which network the existing nodes live on (compose uses "w4f") -if docker inspect w4f-node-a >/dev/null 2>&1; then - NET=$(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}' w4f-node-a 2>/dev/null || echo "$NET") -fi - -start_one() { - local name="$1" alias="$2" cfg="$3" bp="$4" vp="$5" - docker rm -f "$name" >/dev/null 2>&1 || true - docker run -d --name "$name" --user "$(id -u):$(id -g)" \ - --network "$NET" --network-alias "$alias" \ - -p "$bp:$bp" -p "$vp:$vp" \ - -v "$FRP/frps:/app/frps:ro" -v "$PWD/$cfg:/app/frps.toml:ro" \ - --restart unless-stopped --entrypoint /app/frps \ - debian:bookworm-slim -c /app/frps.toml >/dev/null - echo " $name up ($alias, bindPort $bp, vhost $vp) on network $NET" -} - -echo "== starting mock frps nodes on network $NET ==" -start_one w4f-frps2 frps2 frps2.toml 7001 7081 -start_one w4f-frps3 frps3 frps3.toml 7002 7082 - -echo "== mock frps nodes up ==" -docker ps --filter "name=w4f-frps" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" diff --git a/deploy/test-forward.sh b/deploy/test-forward.sh deleted file mode 100755 index 9f6c109..0000000 --- a/deploy/test-forward.sh +++ /dev/null @@ -1,230 +0,0 @@ -#!/usr/bin/env bash -# test-forward.sh — exercise the token-ring forwarding dispatch pipeline. -# -# Submits 4 forwards via PUT /canvas (localOnly=false → ring tasks): -# r-frps → frps:7000 (real, expect connected) -# r-frps2 → frps2:7001 (real mock, expect connected) -# r-frps3 → frps3:7002 (real mock, expect connected) -# r-dead → 192.0.2.1 (dead addr, expect dispatch+worker-start, conn failed) -# -# Verifies: pending→0, topology claimed (with ownerId), worker running on the -# claiming node, frpc "login to server success" for real targets, dead target -# worker running but conn failed. Then revokes (restore canvas) and verifies -# topology cleared + worker stopped. -# -# Run after the cluster is up: bash deploy/test-forward.sh -set -euo pipefail -cd "$(dirname "$0")" - -AUTH=admin:admin123 -A=http://localhost:7501 -B=http://localhost:7502 -C=http://localhost:7503 -D=http://localhost:7504 -E=http://localhost:7505 -NODE_PORTS=("$A" "$B" "$C" "$D" "$E") -RESULTS=$(mktemp -t w4f-results.XXXX) -trap 'rm -f "$RESULTS"' EXIT - -# ---- helpers ---- -cget() { curl -s -u "$AUTH" "$1/api/manager$2"; } -cput() { curl -s -X PUT -u "$AUTH" -H 'Content-Type: application/json' --data "$3" "$1/api/manager$2"; } -cpost() { curl -s -X POST -u "$AUTH" -H 'Content-Type: application/json' --data "$3" "$1/api/manager$2"; } -# py '' reads stdin json as d; stmt may use d -py() { python3 -c "import sys,json; d=json.load(sys.stdin); print($1)"; } - -ok() { printf " \033[32m✓\033[0m %s\n" "$1"; } -fail() { printf " \033[31m✗\033[0m %s\n" "$1"; } -info() { printf " · %s\n" "$1"; } - -echo "╭──────────────────────────────────────────────╮" -echo "│ w4f forwarding dispatch pipeline test │" -echo "╰──────────────────────────────────────────────╯" - -# ---- preflight: cluster + mock frps up ---- -if ! docker ps --format '{{.Names}}' | grep -q '^w4f-node-a$'; then - fail "w4f-node-a not running — start the cluster first: bash deploy/run-cluster.sh" - exit 1 -fi -bash "$PWD/start-mock-frps.sh" >/dev/null 2>&1 || true - -# ---- 1. confirm ring has 5 nodes (wait for convergence) ---- -echo "[1] cluster ring health (expect 4 nodes)" -deadline=$(( $(date +%s) + 30 )) -NN=0 -while [ $(date +%s) -lt $deadline ]; do - RING=$(cget "$A" /cluster/ring) - NN=$(py 'len(d.get("nodes",[]))' <<< "$RING") - if [ "$NN" -ge 4 ]; then break; fi - sleep 1 -done -if [ "$NN" -ge 4 ]; then ok "ring has $NN nodes"; else fail "ring has $NN nodes (expected ≥4)"; exit 1; fi - -# ---- 1.5 join key security (wrong key → 403, correct key present) ---- -echo "[1.5] join key security" -KEY=$(py 'd.get("nodeKey","")' <<< "$RING") -if [ -n "$KEY" ]; then ok "nodeKey present in snapshot (${KEY:0:8}…)"; else fail "no nodeKey in ring snapshot"; fi -CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST -u "$AUTH" -H 'Content-Type: application/json' \ - --data '{"id":"evil","addr":"evil:7500","version":"0.1.0","joinKey":"wrong-key"}' \ - "$A/api/manager/cluster/join") -if [ "$CODE" = "403" ]; then ok "wrong join key → 403 (rejected)"; else fail "wrong join key → HTTP $CODE (expected 403)"; fi -# empty key → 403 too -CODE2=$(curl -s -o /dev/null -w "%{http_code}" -X POST -u "$AUTH" -H 'Content-Type: application/json' \ - --data '{"id":"evil","addr":"evil:7500","version":"0.1.0","joinKey":""}' \ - "$A/api/manager/cluster/join") -if [ "$CODE2" = "403" ]; then ok "empty join key → 403 (rejected)"; else fail "empty join key → HTTP $CODE2 (expected 403)"; fi - -# ---- 2. snapshot original canvas (for cleanup/restore) ---- -echo "[2] snapshot canvas" -ORIG=$(cget "$A" /canvas) -CNT=$(echo "$ORIG" | py 'len((d.get("locals") or [])) + len((d.get("remotes") or [])) + len((d.get("links") or []))') -info "original canvas: $CNT entries (locals+remotes+links)" - -# ---- 3. build merged canvas with 4 forwards + PUT ---- -echo "[3] submit 4 forwards (PUT /canvas on node-a)" -MERGED=$(python3 -c ' -import json, sys -orig = json.load(sys.stdin) -locals_ = orig.get("locals", []) or [] -remotes = orig.get("remotes", []) or [] -links = orig.get("links", []) or [] - -# de-dup by name so re-runs are idempotent -have_l = {l["name"] for l in locals_} -have_r = {r["name"] for r in remotes} -if "backend-svc" not in have_l: - locals_.append({"name":"backend-svc","ip":"backend","port":8080,"protocol":"tcp","localOnly":False}) - -new_rems = [ - ("r-frps", "frps", 7000), - ("r-frps2", "frps2", 7001), - ("r-frps3", "frps3", 7002), - ("r-dead", "192.0.2.1", 7000), -] -for nm, ip, port in new_rems: - if nm not in have_r: - remotes.append({"name":nm,"ip":ip,"port":port,"token":"demo-token","enabled":True}) - -ports = {"r-frps":18091,"r-frps2":18092,"r-frps3":18093,"r-dead":18094} -have_link = {(l["local"],l["remote"]) for l in links} -for nm in ports: - key = ("backend-svc", nm) - if key not in have_link: - links.append({"local":"backend-svc","remote":nm,"remotePort":ports[nm]}) - -print(json.dumps({"locals":locals_,"remotes":remotes,"links":links})) -' <<< "$ORIG") - -cput "$A" /canvas "$MERGED" >/dev/null -ok "canvas submitted (1 local → 4 remotes, localOnly=false)" - -# the 4 remotes we expect to see claimed -declare -A EXPECT_PORT=( [r-frps]=18091 [r-frps2]=18092 [r-frps3]=18093 [r-dead]=18094 ) -declare -A EXPECT_CONN=( [r-frps]=connected [r-frps2]=connected [r-frps3]=connected [r-dead]=failed ) -declare -A OWNER= # remote name -> node port URL - -# ---- 4. poll ring: pending → 0 + topology has our 4 ---- -echo "[4] wait for dispatch: pending → claimed into topology" -deadline=$(( $(date +%s) + 45 )) -claimed=0 -while [ $(date +%s) -lt $deadline ]; do - RING=$(cget "$A" /cluster/ring) - pend=$(py 'len(d.get("pending",[]))' <<< "$RING") - # count our remotes present in topology (python — robust vs empty arrays) - cnt=$(py 'sum(1 for t in d.get("topology",[]) if t["remote"]["name"] in ("r-frps","r-frps2","r-frps3","r-dead"))' <<< "$RING") - if [ "$cnt" -ge 4 ]; then ok "all 4 in topology (pending=$pend)"; claimed=1; break; fi - sleep 1 -done -if [ "$claimed" -ne 1 ]; then - fail "only $cnt/4 reached topology after 45s (pending=$pend)" - RING=$(cget "$A" /cluster/ring); info "ring: $(py 'json.dumps({"pending":len(d.get("pending",[])),"topo":[t["remote"]["name"] for t in d.get("topology",[])]})' <<< "$RING")" - exit 1 -fi -info "topology: $(cget "$A" /cluster/ring | py '",".join(t["remote"]["name"]+"@"+t["ownerId"][:8] for t in d.get("topology",[]))')" - -# ---- 5. find owner node per remote + assert worker running ---- -echo "[5] locate claiming node + assert worker running" -for nm in r-frps r-frps2 r-frps3 r-dead; do - found="" - deadline=$(( $(date +%s) + 40 )) - while [ $(date +%s) -lt $deadline ]; do - for P in "${NODE_PORTS[@]}"; do - ST=$(cget "$P" /status) - # does this node have a profile named $nm with process.state=running? - run=$(py 'next((1 for p in d.get("profiles",[]) if p["name"]=="'"$nm"'" and p.get("process",{}).get("state")=="running"),0)' <<< "$ST") - if [ "$run" = "1" ]; then found="$P"; break; fi - done - [ -n "$found" ] && break - sleep 1 - done - if [ -n "$found" ]; then - OWNER[$nm]="$found" - ok "$nm → worker running on $(echo "$found" | sed 's#http://localhost:##')" - else - fail "$nm → no running worker found on any node within 40s" - OWNER[$nm]="" - fi -done - -# ---- 6. connection state + frpc login log ---- -echo "[6] connection state + frpc login log" -for nm in r-frps r-frps2 r-frps3 r-dead; do - P="${OWNER[$nm]}" - [ -z "$P" ] && { fail "$nm skipped (no worker)"; continue; } - ST=$(cget "$P" /status) - conn=$(py 'next((p.get("connState","") for p in d.get("profiles",[]) if p["name"]=="'"$nm"'"),"")' <<< "$ST") - expect="${EXPECT_CONN[$nm]}" - if [ "$nm" = "r-dead" ]; then - # dead: worker running, conn NOT connected - if [ "$conn" != "connected" ]; then ok "$nm connState=$conn (dead, not connected — correct)"; else fail "$nm unexpectedly connected"; fi - else - # real: wait for connected + login log - deadline=$(( $(date +%s) + 40 )) - logged="" - while [ $(date +%s) -lt $deadline ]; do - LOGS=$(cget "$P" /profiles/$nm/logs) - if echo "$LOGS" | grep -q "login to server success"; then logged=1; break; fi - sleep 1 - done - if [ -n "$logged" ]; then ok "$nm frpc logged in (login to server success)"; else fail "$nm no login-success in 40s (connState=$conn)"; fi - fi -done - -# ---- 6.5 tunnel data path for real frps (bonus) ---- -echo "[6.5] tunnel data path (real frps → backend)" -for nm in r-frps r-frps2 r-frps3; do - port="${EXPECT_PORT[$nm]}" - # curl the remotePort from inside the node-a container (same docker net) - body=$(docker exec w4f-node-a sh -c "command -v curl >/dev/null 2>&1 && curl -sS --max-time 4 http://frps$([ "$nm" != "r-frps" ] && echo "2" || echo ""):$port 2>/dev/null || wget -qO- --timeout=4 http://frps$([ "$nm" != "r-frps" ] && echo "2" || echo ""):$port 2>/dev/null" 2>/dev/null || true) - # the backend responds with some body; just check non-empty or a hit - if [ -n "$body" ]; then ok "$nm tunnel :$port → backend returned data"; else info "$nm tunnel :$port no body (may be expected if backend gives empty)"; fi -done - -# ---- 7. revoke (restore original canvas) ---- -echo "[7] revoke: restore original canvas → wait for topology cleared" -cput "$A" /canvas "$ORIG" >/dev/null -ok "canvas restored to original snapshot" -deadline=$(( $(date +%s) + 30 )) -cleared=0 -while [ $(date +%s) -lt $deadline ]; do - RING=$(cget "$A" /cluster/ring) - cnt=$(py 'sum(1 for t in d.get("topology",[]) if t["remote"]["name"] in ("r-frps","r-frps2","r-frps3","r-dead"))' <<< "$RING") - if [ "$cnt" -eq 0 ]; then ok "topology cleared of all 4 forwards"; cleared=1; break; fi - sleep 1 -done -[ "$cleared" -ne 1 ] && fail "topology still has $cnt forwards after 30s" - -# assert workers stopped on owners -for nm in r-frps r-frps2 r-frps3 r-dead; do - P="${OWNER[$nm]}"; [ -z "$P" ] && continue - ST=$(cget "$P" /status) - run=$(py 'next((1 for p in d.get("profiles",[]) if p["name"]=="'"$nm"'" and p.get("process",{}).get("state")=="running"),0)' <<< "$ST") - if [ "$run" = "0" ]; then ok "$nm worker stopped on $(echo "$P" | sed 's#http://localhost:##')"; else fail "$nm worker still running after revoke"; fi -done - -# ---- summary ---- -echo "" -echo "╭── result ──╮" -echo "│ dispatch + claim + worker + connected : verified (3 real frps + 1 dead) " -echo "│ revoke (canvas restore) → topology cleared + workers stopped : verified " -echo "╰──────────────────────────────────────────────────────────────────────────╯" diff --git a/gui-test-screenshots/root___forwards.png b/gui-test-screenshots/root___forwards.png deleted file mode 100644 index b378c03..0000000 Binary files a/gui-test-screenshots/root___forwards.png and /dev/null differ diff --git a/gui-test-screenshots/root___status.png b/gui-test-screenshots/root___status.png deleted file mode 100644 index 97a8507..0000000 Binary files a/gui-test-screenshots/root___status.png and /dev/null differ diff --git a/gui-test-screenshots/root__forwards.png b/gui-test-screenshots/root__forwards.png deleted file mode 100644 index 545330c..0000000 Binary files a/gui-test-screenshots/root__forwards.png and /dev/null differ diff --git a/gui-test-screenshots/root__status.png b/gui-test-screenshots/root__status.png deleted file mode 100644 index b378c03..0000000 Binary files a/gui-test-screenshots/root__status.png and /dev/null differ diff --git a/gui-test-screenshots/root_index.png b/gui-test-screenshots/root_index.png deleted file mode 100644 index 9fdb453..0000000 Binary files a/gui-test-screenshots/root_index.png and /dev/null differ