From a84219844763ab549015c17487d0fbf91aabf452 Mon Sep 17 00:00:00 2001 From: jianf <2198972886@qq.com> Date: Mon, 17 Aug 2026 21:57:00 +0800 Subject: [PATCH] feat: docker cluster demo (frps+3 nodes, M6 auto binary pull, tunnel e2e) + fix SelfAddr/W4F_HOST/bootstrap retry, -frpc flag --- .gitignore | 11 +++++ deploy/Dockerfile | 21 +++++++++ deploy/README.md | 46 +++++++++++++++++++ deploy/compose.yaml | 88 +++++++++++++++++++++++++++++++++++++ deploy/demo-backend/main.go | 16 +++++++ deploy/frps.toml | 16 +++++++ deploy/run-cluster.sh | 34 ++++++++++++++ internal/httpapi/server.go | 8 +++- 8 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 deploy/Dockerfile create mode 100644 deploy/README.md create mode 100644 deploy/compose.yaml create mode 100644 deploy/demo-backend/main.go create mode 100644 deploy/frps.toml create mode 100755 deploy/run-cluster.sh diff --git a/.gitignore b/.gitignore index 1a7a137..5f827da 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,14 @@ data/ # pi agent local state .pi/ + +# docker demo staging (frp binaries) +frp-bin/ +deploy/frp-bin/ + +# demo runtime data + compiled artifacts (regenerable) +deploy/data/ +deploy/artifacts/ + +# vendored deps (regenerable; keep out of git) +vendor/ diff --git a/deploy/Dockerfile b/deploy/Dockerfile new file mode 100644 index 0000000..087d606 --- /dev/null +++ b/deploy/Dockerfile @@ -0,0 +1,21 @@ +# 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 new file mode 100644 index 0000000..1b5d6f8 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,46 @@ +# 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 new file mode 100644 index 0000000..df026a4 --- /dev/null +++ b/deploy/compose.yaml @@ -0,0 +1,88 @@ +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 + + 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 new file mode 100644 index 0000000..1e9dcb0 --- /dev/null +++ b/deploy/demo-backend/main.go @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..177d953 --- /dev/null +++ b/deploy/frps.toml @@ -0,0 +1,16 @@ +# 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/run-cluster.sh b/deploy/run-cluster.sh new file mode 100755 index 0000000..f2f1ae7 --- /dev/null +++ b/deploy/run-cluster.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# w4f demo cluster: node-a seeds frpc cache, node-b/c auto-pull from neighbors (M6). +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; 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 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 + +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 + +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 + +echo "== cluster up ==" +docker ps --filter "name=w4f-" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" \ No newline at end of file diff --git a/internal/httpapi/server.go b/internal/httpapi/server.go index 4c8d31b..c439e67 100644 --- a/internal/httpapi/server.go +++ b/internal/httpapi/server.go @@ -270,7 +270,9 @@ func (h *Handler) handleClusterNodes(w http.ResponseWriter, r *http.Request) { http.Error(w, "cluster registry not enabled", http.StatusNotFound) return } - // Self node goes first so a querier can identify us by Nodes[0]. + // Self node goes first so a querier can identify us by Nodes[0]. The + // version we advertise is the newest locally cached frpc (falling back to + // the configured binary path), so peers can bootstrap the same version. type nodeResp struct { Addr string `json:"addr"` Cache []string `json:"cache,omitempty"` @@ -278,7 +280,9 @@ func (h *Handler) handleClusterNodes(w http.ResponseWriter, r *http.Request) { } out := make([]nodeResp, 0, 1+len(h.Cluster.NodeList())) ver := "" - if h.BinaryPath != nil { + if cs := h.Cluster.CachedVersions(); len(cs) > 0 { + ver = cs[len(cs)-1] + } else if h.BinaryPath != nil { ver = frpcVersionOf(h.BinaryPath()) } out = append(out, nodeResp{Addr: h.SelfAddr, Cache: h.Cluster.CachedVersions(), Version: ver})