mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-11-05 16:57:19 +00:00
feat: Phase C 完成 — ModelRouter 风格 UI 重设计 + 模拟 frps 测试 + lastSync 修复 + 集群作坊搭建
- 主题: sakura×frost 玻璃拟态 (theme.css) + SCSS 变量重映射 - 侧栏: 玻璃侧栏 246px + 渐变品牌区 + 面包屑导航 - 状态页: 玻璃 KPI 卡 + 远程节点/本地服务卡片网格 - 集群页: 英雄玻璃卡 + 横向环拓扑链 + 待办命令/活跃拓扑/日志区 - 令牌环: 新增 lastSync 上次同步时间替代周期计数 - 模拟 frps: frps2/frps3 容器 + test-forward.sh 全链路验证脚本 - 修复: BinaryPath 空导致 worker 不启动, 撤销仅撤第一个 link, 任务复活风暴 (published 追踪)
This commit is contained in:
@ -12,6 +12,32 @@ services:
|
||||
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
|
||||
|
||||
16
deploy/frps2.toml
Normal file
16
deploy/frps2.toml
Normal file
@ -0,0 +1,16 @@
|
||||
# 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
|
||||
16
deploy/frps3.toml
Normal file
16
deploy/frps3.toml
Normal file
@ -0,0 +1,16 @@
|
||||
# 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
|
||||
@ -21,14 +21,20 @@ docker run -d --name w4f-backend --user $(id -u):$(id -g) --network $NET --netwo
|
||||
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 --restart unless-stopped --entrypoint /app/webui4frpc debian:bookworm-slim -addr=0.0.0.0:7500 -user=admin -password=admin123 -workdir=/data
|
||||
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
|
||||
|
||||
echo "== starting node-b (no frpc; peer 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 --restart unless-stopped --entrypoint /app/webui4frpc debian:bookworm-slim -addr=0.0.0.0:7500 -user=admin -password=admin123 -workdir=/data -peer=node-a:7500
|
||||
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
|
||||
|
||||
echo "== starting node-c (no frpc; peer node-b) =="
|
||||
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 --restart unless-stopped --entrypoint /app/webui4frpc debian:bookworm-slim -addr=0.0.0.0:7500 -user=admin -password=admin123 -workdir=/data -peer=node-b:7500
|
||||
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-b:7500
|
||||
|
||||
echo "== cluster up =="
|
||||
docker ps --filter "name=w4f-" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||||
33
deploy/start-mock-frps.sh
Executable file
33
deploy/start-mock-frps.sh
Executable file
@ -0,0 +1,33 @@
|
||||
#!/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}}"
|
||||
208
deploy/test-forward.sh
Executable file
208
deploy/test-forward.sh
Executable file
@ -0,0 +1,208 @@
|
||||
#!/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
|
||||
NODE_PORTS=("$A" "$B" "$C")
|
||||
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 '<stmt>' 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 3 nodes ----
|
||||
echo "[1] cluster ring health"
|
||||
RING=$(cget "$A" /cluster/ring)
|
||||
NN=$(py 'len(d.get("nodes",[]))' <<< "$RING")
|
||||
if [ "$NN" -ge 3 ]; then ok "ring has $NN nodes"; else fail "ring has $NN nodes (expected ≥3)"; exit 1; 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 "╰──────────────────────────────────────────────────────────────────────────╯"
|
||||
Reference in New Issue
Block a user