Files
webui4frpc/docs/cluster-api.md
jianf eda9bb9597 feat: cluster reliability (leader failover, crash rejoin, key exchange) + auth/users + canvas/forwards enhancements + comprehensive README + API docs
- Cluster: forwardToNext offline detection (leader+non-leader), WatchLeader 1s heartbeat fallback, 409 for standalone nodes, Node.NodeKey key exchange via token ring, ClusterPeers persistence + auto-rejoin, Forward delegates to forwardToNext (bugfix)
- Auth: Basic Auth (flag-creds fast path) + bcrypt users (admin/viewer) + Bearer API keys (read/write/admin scope)
- Frontend: UsersView (accounts+API keys), ClusterView (ring/nodeKey/tasks/topology/log), StatusView (group management, per-proxy status), CanvasView (edge toggle/group), PortEdge (disabled/group labels)
- API: handlers split (canvas/forwards/users/logs), canvas export/import, forwards group start/stop/assign/delete, cluster endpoints
- Docs: comprehensive README rewrite (all flags/APIs/auth/cluster), docs/cluster-api.md (cluster management API reference)
- Deploy: run-cluster.sh now 4-node ring + 1 isolated standalone, test-forward.sh updated for 4 nodes
- Removed plan.md (design notes consolidated into README + API docs)
2026-08-19 21:09:24 +08:00

9.3 KiB
Raw Blame History

webui4frpc 集群管理 API

所有接口前缀 /api/manager,使用 Basic Auth 认证(-user / -password 启动参数)。

权限分级:

  • readviewer + read-scope API key + admin 均可访问
  • writewrite-scope API key + admin启动参数的 Basic Auth 凭证解析为 admin

1. 查看集群状态

GET /cluster/ring

返回当前节点的令牌环快照(领导者、轮次、节点表、待办任务、拓扑、日志、本机 nodeKey

权限read

响应

{
  "selfId": "node-a:7500",
  "leaderId": "node-a:7500",
  "cycle": 152,
  "lastSync": 1787141340,
  "roundDelayMs": 2000,
  "nodeKey": "ed67ca6b181b3cf2...",
  "nodes": [
    {
      "id": "node-a:7500",
      "addr": "node-a:7500",
      "isLeader": true,
      "alive": true,
      "load": { "memPct": 17.5, "netPct": 10, "forwards": 0 },
      "version": "0.1.0",
      "nodeKey": "ed67ca6b181b3cf2...",
      "lastSeen": 1787141340
    }
  ],
  "pending": [],
  "topology": [
    {
      "id": "t1",
      "local": { "name": "web", "port": 8080 },
      "remote": { "name": "frps", "addr": "frps:7000" },
      "link": { "remotePort": 8080 },
      "owner": "node-b:7500",
      "active": true
    }
  ],
  "log": [
    { "seq": 1, "node": "node-a:7500", "kind": "leader.change", "detail": {"leader":"node-a:7500"}, "ts": 1787141300 }
  ]
}

字段说明

字段 说明
selfId 本节点 IDhost:port
leaderId 当前 leader 节点 ID
cycle 令牌环当前轮次(每完成一圈 +1
roundDelayMs 轮次延迟毫秒leader 每轮探测后更新
nodeKey 本节点的准入密钥(新节点加入本节点时需提供此密钥)
nodes[] 环中所有节点(含离线的),按插入顺序排列(即环顺序)
nodes[].nodeKey 该节点的准入密钥(随令牌环交换,用于宕机重联)
pending[] 待摘取的转发任务
topology[] 活跃转发拓扑(含 owner 归属)
log[] 集群事件日志(增量同步后的本地视图)

2. 创建集群

POST /cluster/create

将本节点重置为全新的独立 leader单节点环。生成准入密钥nodeKey其他节点可通过此密钥加入。

权限write

前置条件:本节点不能是多人环成员(已是成员返回 409

请求体:无

响应:同 GET /cluster/ring 的快照

错误

状态码 说明
409 已是多人环成员,需先脱离集群

示例

curl -X POST -u admin:admin123 http://localhost:7501/api/manager/cluster/create

3. 加入集群(本节点发起)

POST /cluster/join-ring

本节点作为新节点,向目标对端发起加入请求。对端验证密钥后将本节点插入环,返回完整环状态供本节点采纳。

权限write

前置条件:本节点不能已是多人环成员(已是成员返回 409

请求体

{
  "addr": "192.168.1.10:7500",
  "joinKey": "ed67ca6b181b3cf2..."
}
字段 说明
addr 对端节点的真实可路由地址IP:port 或域名:port
joinKey 对端节点的 nodeKey从对端的集群页或 GET /cluster/ring 获取)

响应:同 GET /cluster/ring 的快照

错误

状态码 说明
400 addrjoinKey 为空
409 已是多人环成员
502 对端不可达或加入失败(密钥错误返回 403、网络超时等

示例

curl -X POST -u admin:admin123 http://localhost:7502/api/manager/cluster/join-ring \
  -H 'Content-Type: application/json' \
  -d '{"addr":"192.168.1.10:7500","joinKey":"ed67ca6b181b3cf2..."}'

4. 加入集群(对端接收)

POST /cluster/join

节点间内部接口:新节点通过 POST /cluster/join-ring 间接调用此接口。也可直接调用(例如用 curl 模拟新节点加入)。

接收新节点的加入请求,验证密钥后将新节点排在自己后面(成为自己的后继),返回环状态。

权限write

请求体JoinInfo

{
  "id": "node-b:7500",
  "addr": "node-b:7500",
  "version": "0.1.0",
  "cache": ["0.54.0"],
  "joinKey": "ed67ca6b181b3cf2..."
}

响应

{
  "state": { /* 完整 State环状态 */ }
}

错误

状态码 说明
400 JSON 解析失败
403 joinKey 为空或不匹配本节点的 nodeKey

5. 提交转发任务

POST /cluster/task

将一个转发任务附加到令牌,由负载最低的节点摘取并创建 frpc worker。

权限write

请求体

{
  "local":  { "name": "web",  "port": 8080,  "type": "tcp" },
  "remote": { "name": "frps", "addr": "frps:7000", "enabled": true },
  "link":   { "remotePort": 8080, "localPort": 8080 }
}

响应

{
  "task": {
    "id": "t1",
    "local":  { "name": "web", "port": 8080, "type": "tcp" },
    "remote": { "name": "frps", "addr": "frps:7000" },
    "link":   { "remotePort": 8080 },
    "created": 1787141340
  }
}

任务随令牌环行,负载最低的节点摘取后:

  1. 持久化 local/remote/link 到本地 store
  2. 拉起 frpc worker
  3. 写入拓扑(topology[].owner = 摘取节点
  4. 下一轮令牌全网收敛一致

6. 移除节点

POST /cluster/node-remove

发布 node.remove 命令到令牌。令牌传递到被移除节点自身时,该节点执行自移除:

  1. 将自己负责的转发任务重新追加为待办
  2. 修改拓扑与转发链、移除自身
  3. 令牌传递给自身原本的下一家
  4. 脱离后清空本地集群视图(仅保留 localOnly 转发)

权限write

请求体

{ "id": "node-c:7500" }

响应

{
  "task": {
    "id": "t2",
    "type": "node.remove",
    "created": 1787141345
  }
}

被移除节点脱离集群后:

  • nodeKey 清空(不再有效)
  • ClusterPeers 清空(重启不自动重联)
  • 本地画布仅保留 localOnly 转发

7. 令牌中继 + 心跳

POST /cluster/token

节点间内部接口:环上节点之间传递令牌 + leader 上邻居心跳探测。

权限write

两种模式

令牌传递(有 body

请求体为 cluster.Token JSON。本节点

  1. OnToken:采纳令牌中的集群状态 → 运行命令 → 追加自身信息
  2. leader → Sendbump cycle + 转发);非 leader → Forward(转发)
  3. 异步转发,立即返回 200 + 更新后的 token

响应:更新后的 Token JSON

心跳探测(空 body

leader 的上邻居每秒探测 leader 存活。空 body → 心跳。

  • leader 在线且为多人环 → 200 OK
  • leader 已重启/脱离standalone ≤1 节点)→ 409 Conflict(上邻居感知 leader 已退出 → 晋升自身为新 leader + StartRing

错误

状态码 说明
409 standalone 节点拒绝心跳leader 已退出,触发 failover

8. 集群节点 + 二进制缓存

GET /cluster/nodes

列出本节点及已注册的集群对端,含各自缓存的 frpc 版本。

权限read

响应

{
  "nodes": [
    { "addr": "node-a:7500", "version": "0.54.0", "cache": ["0.54.0", "0.53.0"] },
    { "addr": "node-b:7500", "version": "0.54.0", "cache": [] }
  ]
}

GET /cluster/cache

查看本节点缓存的 frpc 版本详情。

权限read

POST /cluster/cache

裁剪缓存,保留最新 N 个版本。

权限writeGET 为 readPOST 需 write

请求体

{ "keep": 3 }

9. 日志

GET /node/logs

返回本节点所有 frpc worker 的日志尾部。

权限read

响应

{
  "node": "node-a:7500",
  "workers": [
    { "name": "frps", "remote": "frps", "state": "running", "lines": "..." }
  ]
}

GET /cluster/logs/export

并行拉取所有 alive 节点的 worker 日志,聚合为 JSON 附件下载。不可达节点标记 error 但不中断。

权限read

响应Content-Disposition: attachment; filename="worker-logs.json"


宕机重联行为

节点宕机重启时的自动重联逻辑(非 API 接口,内部行为

  1. 读取缓存拓扑:启动时从 store.Settings.ClusterPeers 读取上次令牌周期持久化的对端列表([{addr, key}, ...]
  2. 缓存优先于 -peer:如有缓存拓扑,逐个尝试 rejoin用对端的 key 认证);无缓存才用 -peer + -join-key 首次引导
  3. 重试:每 10s 重试5min 超时后回退 CreateClusterstandalone
  4. 显式脱离不重联:通过 POST /cluster/node-remove 脱离集群时清除 ClusterPeers → 重启后不自动重联

容错行为

场景 检测机制 恢复动作
非leader节点宕机 forwardToNext 发送失败 → MarkOffline → OfflineReassign 环跳过死亡节点,任务重挂为待办
leader 宕机(发送时) forwardToNext 发送给 leader 失败 → MarkOffline 上邻居晋升为新 leader + StartRing
leader 宕机(收到令牌后) WatchLeader 心跳失败/409 → MarkOffline 上邻居晋升 + StartRing兜底
leader 快速重启 心跳得到 409standalone 上邻居晋升 + StartRing
令牌丢失 WatchTokenLoss 超时LossTimeout leader 重发令牌