diff --git a/README.md b/README.md index f8352e9..7e3b01f 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,11 @@ - **Scratch 风格画布**:本地转发项(local)与远程服务器(remote)可视化连线,一个 local 可连多个 remote,一个 remote 可连多个 local - **曲线连线 + 端口标签**:每条连线独立曲线,标签为远程端口,可拖拽调整曲线位置,端口可点开编辑 +- **画布冲突检查**:保存时自动检测远程端口/域名重复,避免配置冲突 - **边开关与分组**:画布边上可一键禁用/启用单条转发,可打分组标签(状态页按分组管理、一键启停整组) +- **Del 键/× 按钮删除**:选中连线按 Delete 键或点击端口标签红 × 删除,确认后标记脏 - **一键生成配置**:保存画布即渲染每台 remote 的 frpc JSON 配置,自动拉起/重启对应 worker -- **画布导入导出**:支持 JSON 备份/恢复 +- **画布导入导出**:支持 JSON 信封备份/恢复(含 provenance 元数据) ### frpc 能力 @@ -27,6 +29,10 @@ - **令牌环协议**:若干 webui4frpc 节点组成协作网络;令牌按固定顺序传递,每节点持完整集群信息,单轮即全网收敛 - **负载最低者摘取**:新转发任务附加在令牌中,由负载最低的节点自行摘取并创建 frpc worker + - 主负载信号:当前节点拥有的转发数(权重 ×100) + - 次级信号:**NIC 网络接口利用率**(从 `/proc/net/dev` 增量 + `/sys/class/net/*/speed` 计算,排除了 lo/docker/veth 等虚拟接口) + - 同转发数节点间由内存占用 + 网络饱和度打破平局 +- **per-forward 独立进程**:每条转发拥有独立的 frpc 配置文件和 frpc 进程(worker 键 = `local~remote~port`),单条转发故障不影响兄弟,消除旧模型的多节点 `proxy already exists` 冲突 - **容错自愈**:leader / 非 leader 节点宕机 → 自动检测 → 环跳过死亡节点、任务重挂;leader 宕机 → 上邻居晋升为新 leader - **宕机自动重联**:节点宕机重启后通过缓存的集群拓扑(对端地址 + 准入密钥)自动重联,显式脱离集群的节点不自动重联 - **准入密钥**:每节点有 nodeKey,新节点加入需提供对端密钥;密钥随令牌环交换,全节点互通 @@ -34,10 +40,11 @@ ### 认证与权限 -- **Basic Auth**:启动参数配置管理员账号密码(flag-creds 快速路径,不走 bcrypt) -- **用户管理**:admin / viewer 两种角色,bcrypt 存储,webui 管理 +- **Session-cookie 登录 + Basic Auth**:UI 使用 session-cookie(支持登出),API 保持 Basic Auth + Bearer API Key 兼容 +- **三级角色**:superadmin(账号管理)、admin(全功能除账号)、viewer(只读状态/集群/审计导出) +- **用户管理**:superadmin 角色创建/管理用户,bcrypt 存储,flag 内置账号为 system 只读 - **API Key**:read / write / admin 三级 scope,`w4f_` 前缀,sha256 存储,创建时明文仅展示一次 -- **三级权限**:read(查看)< write(修改)< admin(用户/密钥管理) +- **审计导出**:read 级 view 即可导出 CSV(账号登录记录 / API Key 使用记录 / 集群操作日志 / worker 运行日志) ## 构建 @@ -59,10 +66,10 @@ cd web && npm install && npm run build rm -rf internal/httpapi/dist && cp -r web/dist internal/httpapi/dist ``` -一键全量构建: +一键全量构建(dist 不进入 git,编译前必须构建前端): ```bash -cd web && npm run build && cd .. && cp -r web/dist/* internal/httpapi/dist/ && go build -o webui4frpc ./cmd/webui4frpc +cd web && npm run build && rm -rf internal/httpapi/dist && cp -r web/dist internal/httpapi/dist && go build -o webui4frpc ./cmd/webui4frpc ``` ## 运行 @@ -101,10 +108,10 @@ cd web && npm run build && cd .. && cp -r web/dist/* internal/httpapi/dist/ && g ``` -├── manager.db # SQLite(locals / remotes / links / settings / users / api_keys) -├── configs/.json # 每台 remote 渲染的 frpc 配置 -├── logs/.log # worker 日志(按大小轮转) -└── bin/frpc-/frpc # 一键安装的 frpc +├── manager.db # SQLite(locals / remotes / links / settings / users / api_keys) +├── configs/~~.json # 每条转发的独立 frpc 配置(per-forward 模型) +├── logs/~~.log # 每条转发的独立 worker 日志(按大小轮转) +└── bin/frpc-/frpc # 一键安装的 frpc ``` ## 架构 @@ -113,20 +120,20 @@ cd web && npm run build && cd .. && cp -r web/dist/* internal/httpapi/dist/ && g webui4frpc(单二进制,无 frp 依赖,前端 go:embed) ├── cmd/webui4frpc 入口:flag 解析 → store/process/cluster 装配 → HTTP 服务 + 生命周期 ├── internal/ -│ ├── store SQLite 持久化(locals / remotes / links / settings / users / api_keys) +│ ├── store SQLite 持久化(locals / remotes / links / settings / users / api_keys / sessions) │ ├── canvas 画布模型(local / remote / link 多对多) -│ ├── render 渲染 frpc JSON 配置(tcp/udp/http/https + 高级参数 + LB/健康检查) -│ ├── process worker 进程管理(spawn/stop/restart/自愈/日志轮转) +│ ├── render 渲染 frpc JSON 配置(per-forward 单 proxy + 高级参数 + LB/健康检查) +│ ├── process worker 进程管理(spawn/stop/restart/自愈/日志轮转,worker 键 = local~remote~port) │ ├── install 一键下载官方 frpc(GitHub Releases,含解压安全防护) -│ ├── cluster 令牌环协议(ring engine / leader 选举 / 容错 / 宕机重联 / 二进制分发) -│ └── httpapi REST API + 静态资源(auth / canvas / forwards / cluster / users) +│ ├── cluster 令牌环协议(ring engine / leader 选举 / 容错 / 宕机重联 / 二进制分发 / NIC 负载采样) +│ └── httpapi REST API + 静态资源(auth/session / canvas / forwards / cluster / users / audit 导出) └── web/ Vue 3 SPA(Element Plus + VueFlow 画布 + Pinia) └── src/views/ 状态总览 / 连接配置(画布)/ 集群 / 设置 / 账号与密钥 ``` ## API -所有接口前缀 `/api/manager`,使用 Basic Auth 或 Bearer API Key 认证。权限分 read / write / admin 三级。 +所有接口前缀 `/api/manager`。UI 使用 session-cookie 登录(`/login` `/logout`),API 兼容 Basic Auth 或 Bearer API Key。权限分 read / write / admin 三级,角色 superadmin / admin / viewer 映射到不同 UI 可见性与操作能力。 | 类别 | 方法 | 路径 | 权限 | 说明 | | --- | --- | --- | --- | --- | @@ -156,9 +163,15 @@ webui4frpc(单二进制,无 frp 依赖,前端 go:embed) | | GET | `/cluster/nodes` | read | 集群节点 + 缓存 | | | GET/POST | `/cluster/cache` | read/write | 二进制缓存管理 | | | GET | `/node/logs` | read | 本节点 worker 日志 | -| | GET | `/cluster/logs/export` | read | 导出全节点 worker 日志 | +| | GET | `/cluster/logs/export` | read | 导出全节点 worker 日志(JSON bundle) | +| **审计导出** | GET | `/audit/users.csv` | read | 账号清单 CSV(含最后登录) | +| | GET | `/audit/apikeys.csv` | read | API Key 清单 CSV(含最后使用/过期) | +| | GET | `/audit/cluster-log.csv` | read | 集群操作日志 CSV(时间线 + 原始载荷) | +| | GET | `/audit/worker-logs.csv` | read | 全节点 worker 运行日志 CSV(逐行展开) | | | GET | `/frpc/{version}` | read | 节点间 frpc 二进制分发 | -| **账号** | GET | `/me` | read | 当前身份 | +| **认证** | POST | `/login` | 无 | 登录(表单凭证 → session cookie) | +| | POST | `/logout` | 无 | 登出(清除 session) | +| | GET | `/me` | read | 当前身份 | | | GET/POST | `/users` | admin | 用户列表/创建 | | | PUT/DELETE | `/users/{name}` | admin | 修改/删除用户 | | | GET/POST | `/apikeys` | admin | API Key 列表/创建 | diff --git a/docs/cluster-api.md b/docs/cluster-api.md index 30501bc..1959611 100644 --- a/docs/cluster-api.md +++ b/docs/cluster-api.md @@ -3,6 +3,7 @@ 所有接口前缀 `/api/manager`,使用 Basic Auth 认证(`-user` / `-password` 启动参数)。 权限分级: + - **read**:viewer + read-scope API key + admin 均可访问 - **write**:write-scope API key + admin(启动参数的 Basic Auth 凭证解析为 admin) @@ -17,6 +18,7 @@ **权限**:read **响应**: + ```json { "selfId": "node-a:7500", @@ -55,8 +57,9 @@ ``` **字段说明**: + | 字段 | 说明 | -|---|---| +| --- | --- | | `selfId` | 本节点 ID(`host:port`) | | `leaderId` | 当前 leader 节点 ID | | `cycle` | 令牌环当前轮次(每完成一圈 +1) | @@ -85,11 +88,13 @@ **响应**:同 `GET /cluster/ring` 的快照 **错误**: + | 状态码 | 说明 | |---|---| | 409 | 已是多人环成员,需先脱离集群 | **示例**: + ```bash curl -X POST -u admin:admin123 http://localhost:7501/api/manager/cluster/create ``` @@ -107,27 +112,31 @@ curl -X POST -u admin:admin123 http://localhost:7501/api/manager/cluster/create **前置条件**:本节点不能已是多人环成员(已是成员返回 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' \ @@ -147,6 +156,7 @@ curl -X POST -u admin:admin123 http://localhost:7502/api/manager/cluster/join-ri **权限**:write **请求体**(`JoinInfo`): + ```json { "id": "node-b:7500", @@ -158,6 +168,7 @@ curl -X POST -u admin:admin123 http://localhost:7502/api/manager/cluster/join-ri ``` **响应**: + ```json { "state": { /* 完整 State(环状态) */ } @@ -165,8 +176,9 @@ curl -X POST -u admin:admin123 http://localhost:7502/api/manager/cluster/join-ri ``` **错误**: + | 状态码 | 说明 | -|---|---| +| --- | --- | | 400 | JSON 解析失败 | | 403 | `joinKey` 为空或不匹配本节点的 nodeKey | @@ -181,6 +193,7 @@ curl -X POST -u admin:admin123 http://localhost:7502/api/manager/cluster/join-ri **权限**:write **请求体**: + ```json { "local": { "name": "web", "port": 8080, "type": "tcp" }, @@ -190,6 +203,7 @@ curl -X POST -u admin:admin123 http://localhost:7502/api/manager/cluster/join-ri ``` **响应**: + ```json { "task": { @@ -203,6 +217,7 @@ curl -X POST -u admin:admin123 http://localhost:7502/api/manager/cluster/join-ri ``` 任务随令牌环行,负载最低的节点摘取后: + 1. 持久化 local/remote/link 到本地 store 2. 拉起 frpc worker 3. 写入拓扑(`topology[].owner = 摘取节点`) @@ -215,6 +230,7 @@ curl -X POST -u admin:admin123 http://localhost:7502/api/manager/cluster/join-ri ### `POST /cluster/node-remove` 发布 `node.remove` 命令到令牌。令牌传递到被移除节点自身时,该节点执行自移除: + 1. 将自己负责的转发任务重新追加为待办 2. 修改拓扑与转发链、移除自身 3. 令牌传递给自身原本的下一家 @@ -223,11 +239,13 @@ curl -X POST -u admin:admin123 http://localhost:7502/api/manager/cluster/join-ri **权限**:write **请求体**: + ```json { "id": "node-c:7500" } ``` **响应**: + ```json { "task": { @@ -239,6 +257,7 @@ curl -X POST -u admin:admin123 http://localhost:7502/api/manager/cluster/join-ri ``` 被移除节点脱离集群后: + - `nodeKey` 清空(不再有效) - `ClusterPeers` 清空(重启不自动重联) - 本地画布仅保留 localOnly 转发 @@ -256,7 +275,9 @@ curl -X POST -u admin:admin123 http://localhost:7502/api/manager/cluster/join-ri **两种模式**: #### 令牌传递(有 body) + 请求体为 `cluster.Token` JSON。本节点: + 1. `OnToken`:采纳令牌中的集群状态 → 运行命令 → 追加自身信息 2. leader → `Send`(bump cycle + 转发);非 leader → `Forward`(转发) 3. 异步转发,立即返回 200 + 更新后的 token @@ -264,12 +285,14 @@ curl -X POST -u admin:admin123 http://localhost:7502/api/manager/cluster/join-ri **响应**:更新后的 `Token` JSON #### 心跳探测(空 body) + leader 的上邻居每秒探测 leader 存活。空 body → 心跳。 - leader 在线且为多人环 → `200 OK` - leader 已重启/脱离(standalone ≤1 节点)→ `409 Conflict`(上邻居感知 leader 已退出 → 晋升自身为新 leader + StartRing) **错误**: + | 状态码 | 说明 | |---|---| | 409 | standalone 节点拒绝心跳(leader 已退出,触发 failover) | @@ -285,6 +308,7 @@ leader 的上邻居每秒探测 leader 存活。空 body → 心跳。 **权限**:read **响应**: + ```json { "nodes": [ @@ -307,6 +331,7 @@ leader 的上邻居每秒探测 leader 存活。空 body → 心跳。 **权限**:write(GET 为 read,POST 需 write) **请求体**: + ```json { "keep": 3 } ``` @@ -322,6 +347,7 @@ leader 的上邻居每秒探测 leader 存活。空 body → 心跳。 **权限**:read **响应**: + ```json { "node": "node-a:7500", @@ -355,9 +381,53 @@ leader 的上邻居每秒探测 leader 存活。空 body → 心跳。 ## 容错行为 | 场景 | 检测机制 | 恢复动作 | -|---|---|---| +| --- | --- | --- | | 非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 日志均为内存/文件态,不持久化历史——审计建议定期拉取归档。 diff --git a/internal/store/store.go b/internal/store/store.go index 165485b..f67aa97 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -149,7 +149,7 @@ type Forward struct { type User struct { ID int64 `json:"id"` Username string `json:"username"` - PasswordHash string `json:"-"` // never serialized to clients + PasswordHash string `json:"-"` // never serialized to clients Role string `json:"role"` // "admin" | "viewer" | "superadmin" Enabled bool `json:"enabled"` System bool `json:"system"` // true = flag-synced, UI read-only