与本次 leader 修复无关,是工作区里此前遗留未提交的两个文件,误被 git add -A 卷进上一个提交,此处独立成提交。docs/how-it-works.md 记录了 DB 是唯一真源、 集群只复制环状态不复制配置、forward owner 归属等实测结论;配 scripts/probe-with-virtual-frps.sh 可在隔离环境复现全部结论,不触碰生产。
7.0 KiB
webui4frpc 内部机制(虚拟 frps 实证)
本文所有结论均由隔离的虚拟 frps 环境实测得出,不依赖生产集群。 复现脚本见
scripts/probe-with-virtual-frps.sh。探测时间:2026-10-02 · frp 版本 0.61.1 · 二进制 md5 三机一致(5c4c23d5…)
1. 数据流:DB 是唯一真源,configs/ 是派生产物
API (HTTP)
→ manager.db (SQLite) ← 真源
↓ Render(forward) 渲染
→ data/configs/<name>.json ← 派生产物
↓ exec: frpc -c <cfg>
→ frpc worker 进程
推论(已在生产验证):手改 configs/*.json 会被下一次 DB 写回覆盖。
配置变更必须走 API(PUT /api/manager/canvas 或 /remotes)。
configs/ 目录的文件名格式为 <local>~<remote>~<remotePort>.json。
只有实跑的那一份会被 frpc -c 引用;同名不同端口的历史文件会残留。
2. 集群不复制配置,只复制环状态
internal/cluster/token_transport.go 在环上传的东西:
type Token struct { // ring.go:106
Cycle int64
State State // 节点拓扑 / leader / log delta
Log []LogEntry
}
这是一个领导者选举 + 拓扑状态的载体,不包含 remotes / locals / links。
sessions are not replicated across the ring(session.go:32)——
每个节点有自己的 manager.db。
推论:
- 令牌环 peer 认证用的是 webui Basic 凭据(
SetBasicAuth(User, Pass)), 与 frpsauth.token是两个独立密钥
配置为什么会同步到新节点
Token.State 里除了环状态,还带 Topology map[string]*TopoEntry,
而 TopoEntry 携带完整配置(ring.go:76):
type TopoEntry struct {
TaskID string
OwnerID string
Local store.Local // 完整 local 配置
Remote store.Remote // 完整 remote 配置(含 token)
Link store.Link
Active bool
}
owner 节点 claim 任务时,配置直接写进本地 DB(cmd/webui4frpc/main.go:147):
ClaimFn: func(ctx, tk) error {
st.UpsertLocal(tk.Local)
st.UpsertRemote(tk.Remote)
... st.ReplaceLinks(links) // 去重后替换
// 然后 spawn worker
}
读侧对应 handleCanvasGet(handlers.go:22)的合并:把拓扑里本节点没有的
local/remote/link 补进画布,让每台都看到全集群视图。这是 read-time merge,
不回写 SQLite(避免触发 per-forward stop/start)。
同步边界(重要)
| 内容 | 是否同步 | 机制 |
|---|---|---|
| 已建立 forward 的配置 | ✅ | 随 Topology 环内流动,owner claim 时落库 |
| 环状态(leader / 节点 / log delta) | ✅ | Token.State |
| remote 定义变更(改 token 等) | ⚠️ 走任务 | 不是 DB 快照复制 |
| 仅有定义、从未成为 active forward 的配置 | ❌ | 没有 TopoEntry 就没有载体 |
| WebUI session | ❌ | session.go:32 明确不复制 |
推论:
- "在一台改、集群自动同步"对已建立的 forward 成立——靠
Topology+ClaimFn - 改 frps remote 的 token 会随任务下发到 owner,不必逐台手改
- 但只存在于某台 DB、且无对应 active forward 的配置不会自动过去
- 这也是本项目 frps token 轮换的正确路径:走 API 提交任务,而非逐台改文件
3. forward 的 owner 归属:非 localOnly 由 owner 拉起
startForward(handlers_forwards.go:62)的两条分支:
| forward 类型 | 谁执行 | 机制 |
|---|---|---|
localOnly = true |
本节点直接 Process.Start(key) |
无需环 |
localOnly = false |
拓扑 owner 节点 | Ring.SubmitTask / UpdateTopologyDisabled 投递 |
非 localOnly 分支有一段注释解释了为什么需要专门的 RESTART 任务:
SubmitTaskis idempotency-guarded, and the entry still exists … so the submission is deduped away- the claim path drops any task for a forward that already has an owner
Without it a forward the user can stop but not restart
实测观察:单节点 fresh 集群(cycle: 0、无 peer)下,
POST /forwards/start 对非 localOnly forward 返回 {"ok":true},
但 configs/ 为空、connState=not_started——环不转时任务无人 claim。
单节点环境应使用 localOnly: true 的 forward,或加入多节点集群。
这是设计行为,不是缺陷。
4. 三个易踩的坑
4.1 binaryPath 为空 → worker 静默不启动
main.go:89 的解析顺序:
BinaryPath: func() string {
if s, _ := st.Settings(); s.BinaryPath != "" { return s.BinaryPath }
return fallbackBin
}
全新 workdir(未装过 frpc)binaryPath 为空,回退到 fallbackBin。
若该路径也不存在,spawn() 报错,但 POST /forwards/start 仍返回 {"ok":true}
(错误只落在 worker 日志里)。排障时先查:
GET /api/manager/binary/status # {"binaryPath": "..."}
4.2 UpdateSettings 是全量替换
store.go:754 的 UpdateSettings 直接 json.Marshal(st) 覆盖整行。
POST 一个不含 binaryPath 的 settings JSON,会把该字段清空。
改 settings 前必须先 GET 现有值再整体回传。
4.3 画布会自动改写 loopback 地址
applyCanvas(handlers.go:108)对非 localOnly 的 local:
if !canvas.Locals[i].LocalOnly && cluster.IsLoopbackIP(canvas.Locals[i].IP) {
canvas.Locals[i].IP = cluster.RewriteForCluster(canvas.Locals[i].IP)
}
实测:PUT /canvas 传 127.0.0.1 → 落库变成 192.168.2.60(本机 LAN 地址)。
这是为了保证其它节点能连到该 local。localOnly 的 local 保持 loopback 不变。
5. 启动凭据来自命令行 flag
main.go:64 每次启动都 SyncSystemUser(*user, *pass),把 -user/-password
同步进 users 表(system=1)。不传 flag 时用内置默认值,
POST /api/manager/login 需要 JSON body({"username","password"}),
拿到身份后不返回 token——后续用 HTTP Basic。
6. API 速查(实测路径)
| 用途 | 方法 | 路径 |
|---|---|---|
| 登录(返回身份,无 token) | POST | /api/manager/login |
| 全量保存画布(local+remote+link) | PUT | /api/manager/canvas |
| 读取画布 | GET | /api/manager/canvas |
| 启/停单条 forward | POST | /api/manager/forwards/start /stop |
| 分组启停 | POST | /api/manager/forwards/group/start /stop |
| 改转发归属 | POST | /api/manager/forwards/assign |
| 二进制状态 | GET | /api/manager/binary/status |
| 环拓扑 | GET | /api/manager/cluster/ring |
| 节点列表 | GET | /api/manager/cluster/nodes |
认证:HTTP Basic(Authorization: Basic base64(user:pass))。
POST 到 /canvas 会返回 method not allowed——必须用 PUT。
7. 复现
scripts/probe-with-virtual-frps.sh
搭建 frps 0.61.1(bindPort=17000 / vhostHTTPPort=17080 / vhostHTTPsPort=17443,
独立 token),用独立 workdir 拉起 webui4frpc(:17500),全流程不触碰生产。