按用户要求重做:不再由 leader 单独调度,而是**每个节点对自己的转发跑同一条
规则**,超了就自己撤销一条、把任务丢回环上,由环原有的「最闲者认领」决定它
落到哪台。
## 为什么必须是 owner 自己动手(不是洁癖,是实测逼出来的)
真机探针(跑完已删)显示,真实 revoke 之后:
TopologyOwner after revoke = "n1" ← 条目还在(只标记不删除)
n1 Forwards=3 score=310.0 ← 负载根本没降
after step: revoking=true publishing=false
⇒ 一节点的负载**只有它自己的拓扑条目消失时才会下降**,而条目是本地簿记,没有任何
对端可以代它写。leader 从外面只能看见失衡、无法让失衡成为事实——它必须伸手去改
另一个节点的簿记,而这正是这个环到处都在避免的跨节点写(revoke / restart 定向往
owner 也是同一个理由)。
⇒ 而且旧实现**根本跑不起来**:phase 1 判据是「条目消失」,可 mark-don't-remove
让它永远不消失,于是每 120s 打一条 `revoking … timed out; abandoning`,
**phase 2 永不执行,一条转发都不会迁**。开 `W4F_BALANCE_MODE=on` 只会刷日志。
## 规则(替代原来的 ratio 阈值)
当 Forwards >= 2 且 Forwards > ceil(总数/存活节点数) 时,撤销一条
- **自限**:3 条 / 3 台 → owner 2 > 1,卸一条 → 1,`1 > 1` 不成 → 停。
没有上界的话 owner 会把转发**全部卸光**。
- **不会卸空**:低于下限(2)永不撤销。
- 平摊值向上取整(3/3=1,不是 0),否则 1 条 / 3 台的节点也算「超了」。
- **不再用 mem/net 参与判据**:实测 `.30` 空闲时 netPct 能到 55%,Score 会高于
有转发的节点——用 Score 会把纯网络抖动当成负载。
- 节奏用**每节点冷却**(默认 600s)而不是「失衡持续多久」窗口:规则本身已经要求
真实失衡,没有需要平滑的瞬态,只有需要限制的抖动频率。
## 为什么必须是新的 Release 任务类型,不能复用 Revoke
用户停止(Revoke)**保留拓扑条目**作为 disabled 标志的载体;均衡释放必须**删除
条目**,因为 (a) 负载由自己的条目数算出,条目不删负载不降;(b) 每个节点的
duplicate-guard 会拒绝「已有 owner 的转发」的创建任务——保留条目会让**所有节点
都拒领**,转发被永久搁死在刚放弃它的那台机器上。
Release 因此:`Handler.Release`(停 worker,但**不标记 stopped**)→ 删条目 →
`SubmitTask` 以**启用状态**重新发布。它需要自己的 Handler 方法,不能回落到
Revoke:Revoke 会顺手把 link 标成 stopped,而这条转发即将在别的节点上跑。
## 顺带修掉两个既有缺口(实测确认)
1. **`submitMigrate` 发布不出去**:`OnToken` 的 localPending 重合并只保留「从未
publish 过」的本地 pending,而只有 `Add*` 系列会记 `published`。引擎里直接写
`PendingTasks` 的任务会在下一次 adopt 时被静默丢弃。
⇒ 新 `ReleaseTask` 走 `AddPending`。
(`SubmitRestart` 有同样的毛病,但它不在本次范围,未改动——见提交说明末尾。)
2. `Release` 需要绕过 duplicate-claim 守卫(同 Restart),否则「owner 本来就持有
这条转发」这一前提会让每条 Release 都被丢掉。
## 测试(17 条,全绿)
规则:平摊值取整、超了才撤、撤到平摊就停、单节点不撤、低于下限不撤、不撤已停
转发、候选选择确定。
机制:Revoke 留载体 vs Release 删条目(这是两种语义的核心差异)、Release 只由
owner 执行且不消费他人的任务、owner 释放后环的普通认领把它接走。
模式:dry-run 不动任何东西、off 真的什么都不做、未知模式回落 dry-run、
冷却限制抖动频率、确认超时清理。
只读:Report 不变更任何状态、能解释「超了但不动」的原因、均衡节点给出真实答案。
**双向验证 3 条**(改坏→变红→还原→变绿):
- 去掉 Release 的 duplicate-guard 例外 → `the owner released 0 times, want 1`
- 去掉「不超过平摊值」上界 → `2 owned against share 2 must not release`
- Release 改成只标记不删除 → `n1 should now own 2, got 3`
go build / go vet / go test ./... 全绿(go clean -testcache 后逐包实跑)。
## 未做 / 待办
- `SubmitRestart` 的同类发布缺口未修(同样会被 adopt 丢弃),需要单独确认它
在实机上是否真的丢过 restart。
- UI 尚未跟进新报告结构(下一步)。
webui4frpc
AI-assisted: 本项目使用 AI 辅助编程(代码与文档由 AI 协作完成,经人工复核)。 一个独立于 frp 源码的可视化 frpc 控制器。用画布方式把「本地转发项」连到「远程服务器」,自动为每台远程服务器生成 frpc 配置并拉起独立 worker 进程,免去运维反复手写 frpc 配置的麻烦。支持多节点组成令牌环集群,协同分发转发任务。
本仓库不捆绑、不包含任何 frp 源码。frpc 可执行文件由用户一键从官方 GitHub Releases 下载,或手动指定路径。
特性
画布配置
- Scratch 风格画布:本地转发项(local)与远程服务器(remote)可视化连线,一个 local 可连多个 remote,一个 remote 可连多个 local
- 曲线连线 + 端口标签:每条连线独立曲线,标签为远程端口,可拖拽调整曲线位置,端口可点开编辑
- 画布冲突检查:保存时自动检测远程端口/域名重复,避免配置冲突
- 边开关与分组:画布边上可一键禁用/启用单条转发,可打分组标签(状态页按分组管理、一键启停整组)
- Del 键/× 按钮删除:选中连线按 Delete 键或点击端口标签红 × 删除,确认后标记脏
- 一键生成配置:保存画布即渲染每台 remote 的 frpc JSON 配置,自动拉起/重启对应 worker
- 画布导入导出:支持 JSON 信封备份/恢复(含 provenance 元数据)
frpc 能力
- 代理类型:tcp(完整)、http/https(customDomains / subdomain / locations / basicAuth / headerRewrite)
- 传输参数:useEncryption / useCompression / bandwidthLimit / poolCount / transport.protocol(tcp/quic/kcp/websocket)/ TLS
- 负载均衡与健康检查:lbGroup + healthCheck 渲染输出;状态页通过 frpc admin API 拉取 per-proxy 真实状态(running / check failed / wait start …)
- worker 自愈:崩溃自动重启(指数退避),随 webui 启停
- frpc 一键安装:从官方 Releases 下载任意版本 frpc,或手动指定二进制路径
集群模式(令牌环)
- 令牌环协议:若干 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,新节点加入需提供对端密钥;密钥随令牌环交换,全节点互通
- 集群二进制分发:节点间优先互传 frpc 二进制,外部 URL 兜底
认证与权限
- 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 级 view 即可导出 CSV(账号登录记录 / API Key 使用记录 / 集群操作日志 / worker 运行日志)
构建
后端(Go 1.25+)
go build -o webui4frpc ./cmd/webui4frpc
前端(Node 20+)
cd web && npm install && npm run build
前端产物在 web/dist,需拷贝到 internal/httpapi/dist 以便 Go embed 打包进单一二进制:
rm -rf internal/httpapi/dist && cp -r web/dist internal/httpapi/dist
一键全量构建(dist 不进入 git,编译前必须构建前端):
cd web && npm run build && rm -rf internal/httpapi/dist && cp -r web/dist internal/httpapi/dist && go build -o webui4frpc ./cmd/webui4frpc
安装
从 Release 页 下载对应平台的安装包。
Linux (Debian / Ubuntu)
sudo dpkg -i webui4frpc-0.1.3-linux-amd64.deb # 或 linux-arm64.deb
sudo nano /etc/webui4frpc/webui4frpc.env # 改掉默认密码
sudo systemctl restart webui4frpc
Linux (RHEL / Fedora / openEuler / 默认 / 龙蜥)
sudo rpm -i webui4frpc-0.1.3-linux-x86_64.rpm
sudo vi /etc/webui4frpc/webui4frpc.env
sudo systemctl enable --now webui4frpc
安装后自动建 webui4frpc 系统账号、注册 systemd 单元,数据目录在 /var/lib/webui4frpc。
服务以非 root 运行,并启用 ProtectSystem=full / NoNewPrivileges 等加固项。
Windows
双击 webui4frpc-0.1.3-windows-amd64-setup.exe(ARM 设备用 windows-arm64-setup.exe)。
安装向导会询问监听地址与管理员凭据,完成后:
- 程序装到
C:\Program Files\webui4frpc - 开始菜单生成启动快捷方式与卸载项
- 自动添加防火墙入站规则
macOS
sudo installer -pkg webui4frpc-0.1.3-darwin-arm64.pkg -target / # Apple Silicon
sudo installer -pkg webui4frpc-0.1.3-darwin-amd64.pkg -target / # Intel
或直接双击 .pkg 走图形安装器。安装后 launchd 开机自启,配置在
/usr/local/etc/webui4frpc/webui4frpc.env,改完执行:
sudo launchctl kickstart -k system/com.jianfeeeee.webui4frpc
该
.pkg未经 Apple 公证(notarization)。首次打开若被 Gatekeeper 拦下, 在「系统设置 → 隐私与安全」选择仍要打开。
免安装(tar.gz / zip)
不想装服务只想跑一下,用对应的 .tar.gz / .zip,解压即用:
tar xzf webui4frpc-0.1.3-linux-amd64.tar.gz
./webui4frpc -addr 127.0.0.1:7500 -user admin -password 你的密码 -workdir ./data
下载后可用 SHA256SUMS 校验完整性:
sha256sum -c SHA256SUMS --ignore-missing
运行
从源码构建后直接跑:
./webui4frpc -addr 127.0.0.1:7500 -user admin -password admin123 -workdir ./data
打开 http://127.0.0.1:7500/ ,输入账号密码进入画布。首次使用可到「设置」页一键安装 frpc(或手动指定路径),然后回到画布创建 local / remote 并连线、保存。
参数
| 参数 | 默认 | 说明 |
|---|---|---|
-addr |
127.0.0.1:7500 |
监听地址 |
-user / -password |
admin / admin |
Basic 认证(自动同步为 system 管理员账号) |
-workdir |
./webui-frpc |
数据目录(db / 配置 / 日志 / bin) |
-bin |
空 | 初始 frpc 二进制路径(可选,首次启动写入 settings) |
-frpc |
空 | worker 默认 frpc 路径(settings 无值时的回退) |
-peer |
空 | 集群对端地址 host:port(可重复,仅首次引导加入用) |
-join-key |
$W4F_JOIN_KEY |
集群准入密钥 = 对端节点的 nodeKey(与 -peer 配合使用) |
环境变量 W4F_HOST 可覆盖节点对外可达的主机名(Docker DNS / 通配监听场景用)。
启动行为
节点启动时的集群引导优先级:
- 缓存拓扑优先:读取持久化的
ClusterPeers(宕机前最后令牌周期保存的对端列表),逐个尝试 rejoin -peer引导:无缓存拓扑时,用-peer+-join-key加入指定对端(10s 重试、5min 超时)- 新建集群:无缓存无
-peer→CreateCluster成为独立 leader
显式脱离集群(detachAsStandalone)会清除 ClusterPeers → 重启后不自动重联。
数据目录
<workdir>
├── manager.db # SQLite(locals / remotes / links / settings / users / api_keys)
├── configs/<local>~<remote>~<port>.json # 每条转发的独立 frpc 配置(per-forward 模型)
├── logs/<local>~<remote>~<port>.log # 每条转发的独立 worker 日志(按大小轮转)
└── bin/frpc-<v>/frpc # 一键安装的 frpc
架构
webui4frpc(单二进制,无 frp 依赖,前端 go:embed)
├── cmd/webui4frpc 入口:flag 解析 → store/process/cluster 装配 → HTTP 服务 + 生命周期
├── internal/
│ ├── store SQLite 持久化(locals / remotes / links / settings / users / api_keys / sessions)
│ ├── canvas 画布模型(local / remote / link 多对多)
│ ├── render 渲染 frpc JSON 配置(per-forward 单 proxy + 高级参数 + LB/健康检查)
│ ├── process worker 进程管理(spawn/stop/restart/自愈/日志轮转,worker 键 = local~remote~port)
│ ├── install 一键下载官方 frpc(GitHub Releases,含解压安全防护)
│ ├── 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。UI 使用 session-cookie 登录(/login /logout),API 兼容 Basic Auth 或 Bearer API Key。权限分 read / write / admin 三级,角色 superadmin / admin / viewer 映射到不同 UI 可见性与操作能力。
| 类别 | 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|---|
| 状态 | GET | /status |
read | 总览(版本/服务/节点/转发/profiles) |
| 画布 | GET/PUT | /canvas |
read/write | 读写完整画布 |
| GET | /canvas/export |
read | 导出画布备份 | |
| POST | /canvas/import |
write | 导入画布 | |
| 转发 | POST | /forwards/start|stop |
write | 启停单条转发 |
| POST | /forwards/group/start|stop |
write | 启停整组 | |
| POST | /forwards/assign |
write | 修改转发分组 | |
| POST | /forwards/group/delete |
write | 删除分组 | |
| 单资源 | PUT/DELETE | /locals[/{name}] |
write | 增删 local |
| PUT/DELETE | /remotes[/{name}] |
write | 增删 remote | |
| POST/DELETE | /links[/{id}] |
write | 增删 link | |
| Profile | POST | /profiles/{name}/start|stop|restart |
write | worker 启停重启 |
| GET | /profiles/{name}/config|logs |
read | 配置/日志 | |
| 设置 | GET/PUT | /settings |
read/write | 运行策略 |
| GET | /binary/status |
read | frpc 路径 | |
| POST | /binary/install |
write | 安装 frpc | |
| 集群 | GET | /cluster/ring |
read | 令牌环快照 |
| POST | /cluster/create |
write | 创建独立集群 | |
| POST | /cluster/join-ring |
write | 加入集群(本节点发起) | |
| POST | /cluster/join |
write | 接收新节点加入 | |
| POST | /cluster/task |
write | 提交转发任务到环 | |
| POST | /cluster/node-remove |
write | 移除节点 | |
| POST | /cluster/token |
write | 令牌中继 + 心跳 | |
| GET | /cluster/nodes |
read | 集群节点 + 缓存 | |
| GET/POST | /cluster/cache |
read/write | 二进制缓存管理 | |
| GET | /node/logs |
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 二进制分发 | |
| 认证 | 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 列表/创建 | |
| DELETE | /apikeys/{id} |
admin | 吊销 API Key |
集群管理 API 的详细说明(请求体、响应格式、错误码、示例)见 docs/cluster-api.md。
集群模式
多个 webui4frpc 节点可组成令牌环集群协同工作:
- 创建集群:首个节点
CreateCluster成为 leader,生成准入密钥(nodeKey) - 加入集群:在集群页复制对端的 nodeKey,填入「加入集群」对话框(对端地址 + 密钥)
- 任务分发:在任意节点画布上配置
localOnly=false的转发并保存 → 画布差异判断生成命令 → 令牌携带 → 负载最低节点摘取 → 拉起 frpc worker - 容错:节点宕机 → 自动检测 → 环自愈(跳过死亡节点 / leader failover);重启 → 自动重联
- 退出集群:在集群页「退出集群」→ 本节点脱离为 standalone,不自动重联
集群页展示:环拓扑(self/leader 标记)、令牌轮次、节点负载、待办任务、活跃转发拓扑(含 owner 归属)、增量事件日志。
测试
# 后端
go test ./...
# 前端类型检查
cd web && npm run type-check