Files
webui4frpc/docs/cluster-api.md
JianFeeeee 94b6396738 docs: 更新 README 与 cluster-api — per-forward 模型/网络负载/审计CSV/三级角色/session登录
README:
- 特性: per-forward 独立进程、NIC 负载采样、冲突检查、Del 键删除
- 认证: session-cookie 登录 + 三级角色 (superadmin/admin/viewer)
- 审计: 4 个 CSV 导出端点说明
- 构建: dist 不再进 git (编译前需 cp -r)
- 数据目录: 配置/日志路径改为 per-forward 键式
- 架构: 更新模块说明 (worker_key/session/audit)
- API 表: 新增 login/logout + 审计导出 4 端点
- gofmt: store.go 注释对齐

docs/cluster-api.md:
- §9 审计 CSV 导出: 四个端点用法、示例行、公式注入防御说明
2026-08-24 23:52:16 +08:00

434 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 个版本。
**权限**writeGET 为 readPOST 需 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 超时后回退 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 重发令牌 |
---
## 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 日志均为内存/文件态,不持久化历史——审计建议定期拉取归档。