- 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)
9.3 KiB
webui4frpc 集群管理 API
所有接口前缀 /api/manager,使用 Basic Auth 认证(-user / -password 启动参数)。
权限分级:
- read:viewer + read-scope API key + admin 均可访问
- write:write-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 |
本节点 ID(host: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 | addr 或 joinKey 为空 |
| 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
}
}
任务随令牌环行,负载最低的节点摘取后:
- 持久化 local/remote/link 到本地 store
- 拉起 frpc worker
- 写入拓扑(
topology[].owner = 摘取节点) - 下一轮令牌全网收敛一致
6. 移除节点
POST /cluster/node-remove
发布 node.remove 命令到令牌。令牌传递到被移除节点自身时,该节点执行自移除:
- 将自己负责的转发任务重新追加为待办
- 修改拓扑与转发链、移除自身
- 令牌传递给自身原本的下一家
- 脱离后清空本地集群视图(仅保留 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。本节点:
OnToken:采纳令牌中的集群状态 → 运行命令 → 追加自身信息- leader →
Send(bump cycle + 转发);非 leader →Forward(转发) - 异步转发,立即返回 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 个版本。
权限:write(GET 为 read,POST 需 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 接口,内部行为):
- 读取缓存拓扑:启动时从
store.Settings.ClusterPeers读取上次令牌周期持久化的对端列表([{addr, key}, ...]) - 缓存优先于
-peer:如有缓存拓扑,逐个尝试 rejoin(用对端的 key 认证);无缓存才用-peer+-join-key首次引导 - 重试:每 10s 重试,5min 超时后回退 CreateCluster(standalone)
- 显式脱离不重联:通过
POST /cluster/node-remove脱离集群时清除ClusterPeers→ 重启后不自动重联
容错行为
| 场景 | 检测机制 | 恢复动作 |
|---|---|---|
| 非leader节点宕机 | forwardToNext 发送失败 → MarkOffline → OfflineReassign |
环跳过死亡节点,任务重挂为待办 |
| leader 宕机(发送时) | forwardToNext 发送给 leader 失败 → MarkOffline |
上邻居晋升为新 leader + StartRing |
| leader 宕机(收到令牌后) | WatchLeader 心跳失败/409 → MarkOffline |
上邻居晋升 + StartRing(兜底) |
| leader 快速重启 | 心跳得到 409(standalone) | 上邻居晋升 + StartRing |
| 令牌丢失 | WatchTokenLoss 超时(LossTimeout) |
leader 重发令牌 |