# 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 **响应**: ```json { "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 | 已是多人环成员,需先脱离集群 | **示例**: ```bash curl -X POST -u admin:admin123 http://localhost:7501/api/manager/cluster/create ``` --- ## 3. 加入集群(本节点发起) ### `POST /cluster/join-ring` 本节点作为新节点,向目标对端发起加入请求。对端验证密钥后将本节点插入环,返回完整环状态供本节点采纳。 **权限**:write **前置条件**:本节点不能已是多人环成员(已是成员返回 409)。 **请求体**: ```json { "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、网络超时等) | **示例**: ```bash 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`): ```json { "id": "node-b:7500", "addr": "node-b:7500", "version": "0.1.0", "cache": ["0.54.0"], "joinKey": "ed67ca6b181b3cf2..." } ``` **响应**: ```json { "state": { /* 完整 State(环状态) */ } } ``` **错误**: | 状态码 | 说明 | | --- | --- | | 400 | JSON 解析失败 | | 403 | `joinKey` 为空或不匹配本节点的 nodeKey | --- ## 5. 提交转发任务 ### `POST /cluster/task` 将一个转发任务附加到令牌,由负载最低的节点摘取并创建 frpc worker。 **权限**:write **请求体**: ```json { "local": { "name": "web", "port": 8080, "type": "tcp" }, "remote": { "name": "frps", "addr": "frps:7000", "enabled": true }, "link": { "remotePort": 8080, "localPort": 8080 } } ``` **响应**: ```json { "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 **请求体**: ```json { "id": "node-c:7500" } ``` **响应**: ```json { "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 → `Send`(bump 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 **响应**: ```json { "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) **请求体**: ```json { "keep": 3 } ``` --- ## 9. 日志 ### `GET /node/logs` 返回本节点所有 frpc worker 的日志尾部。 **权限**:read **响应**: ```json { "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 超时后回退 CreateCluster(standalone) 4. **显式脱离不重联**:通过 `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 重发令牌 | --- ## 9. 审计 CSV 导出(read 级) 四个只读导出端点,供审计/合规场景下载证据表格。全部返回 `text/csv; charset=utf-8` + `Content-Disposition: attachment`,RFC4180 转义,ISO8601 UTC 时间戳。`= + @ - tab` 开头的单元格自动加 `'` 前缀防御表格软件公式注入。 ### `GET /audit/users.csv` 账号清单(密码哈希永不导出)。 ```csv id,username,role,enabled,system,created_at,last_login_at 1,jianf,admin,true,true,2026-08-23T03:18:03Z,2026-08-24T14:00:00Z ``` ### `GET /audit/apikeys.csv` API Key 清单(仅展示前缀 `w4f_xxxxxxxx…`,明文不可逆)。 ```csv id,key_prefix,owner,label,scope,created_at,last_used_at,expires_at 1,w4f_5hIp…,jianf,ci-key,read,2026-08-24T14:47:54Z,, ``` ### `GET /audit/cluster-log.csv` 本节点的令牌环操作日志时间线(转发创建/撤销、节点加入离开、leader 变更、任务认领)。`detail` 列为人读摘要,`data_json` 为无损原始载荷。 ```csv seq,time_utc,node,kind,detail,data_json 1,2026-08-24T14:51:21Z,192.168.2.60:7500,node.join,"192.168.2.60:7500 @ 192.168.2.60:7500","{""addr"":""192.168.2.60:7500"",""node"":""192.168.2.60:7500""}" ``` ### `GET /audit/worker-logs.csv` 全环 fan-out 收集每节点 frpc worker 日志,**逐行展开**(一行 = 一条日志行),便于在表格软件中按 worker_state 过滤或按 remote 分组统计。不可达节点生成 `ERROR` 行。 ```csv node,worker_key,remote,worker_state,log_line 192.168.2.30:7500,homeagent_device~aliyun-frps~9891,aliyun-frps,running,"564 [I] [client/control.go:168] [...] start proxy success" ``` > 注意:操作日志与 worker 日志均为内存/文件态,不持久化历史——审计建议定期拉取归档。