JianFeeeee b29cd0d186 !1 merge feat/config-file-startup into main
feat: 支持从配置文件启动,口令不再出现在命令行

Created-by: JianFeeeee
Commit-by: JianFeeeee
Merged-by: JianFeeeee
Description: ## 背景
口令原先只能通过 -password 命令行 flag 传入,明文可见于 /proc/<pid>/cmdline 与 systemd 单元文件。

## 改动
- 新增 internal/appconfig:配置解析 / ${VAR} 与 file: 密钥展开 / flag > 文件 > 默认 三级合并(18 个单测)
- main.go 接 -config,默认 <workdir>/config.json($W4F_CONFIG 兜底);日志只打字段来源与脱敏摘要
- 口令与 join_key 在日志中固定 <redacted>;实测启动日志与 /proc/<pid>/cmdline 均无明文
- packaging/(systemd 单元、deb/rpm、macOS pkg、Windows NSIS)与 README 同步改为「只传 -config」
- scripts/criteria/config-startup.sh 判据脚本(正向 + 6 条变异全部咬住)

## 兼容性
未配置口令时保持旧行为(回落内置默认 + 一行 WARNING);配置文件缺失不算错误。坏 JSON / 未知键 / 尾部多余内容一律非零退出,不静默降级成空口令。

## 验证
复核 pass:正向全绿 + 6/6 变异被咬住;与 main 可干净合并(merge-tree,基点 21eb52df)。
注意:CI 里跑判据需先构建前端(internal/httpapi/dist 是 go:embed 的构建期产物,基线即如此)。

See merge request: HeNanMU-EEA/webui4frpc!1
2026-10-23 09:22:13 +08:00

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 等虚拟接口)
    • 同转发数节点间由内存占用 + 网络饱和度打破平局
  • 负载均衡(自导式):每个节点对自己的转发跑同一条规则 —— 持有数超过 ceil(总转发数 / 存活节点数) 且至少 2 条时,自行撤销一条、把任务丢回环上, 再由环按「最闲者优先」重新认领
    • 没有中心调度:一节点的负载只有它自己的拓扑条目消失时才会下降,而条目是 本地簿记,没有对端能代它写,所以只能由持有者自己动手
    • 规则自限:撤到平摊值即停(否则会把转发卸光),且低于下限永不撤、不会卸空节点
    • 每节点默认 10 分钟冷却,限制抖动频率;默认 dry-run(只报告不释放)
    • 集群页每节点一个开关:关闭 / 观察模式 / 启用,可持久化
  • 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.4-linux-amd64.deb   # 或 linux-arm64.deb
sudo nano /etc/webui4frpc/config.json           # 改掉默认密码(见「从配置文件启动」)
sudo systemctl restart webui4frpc

Linux (RHEL / Fedora / openEuler / 默认 / 龙蜥)

sudo rpm -i webui4frpc-0.1.4-linux-x86_64.rpm
sudo vi /etc/webui4frpc/config.json
sudo systemctl enable --now webui4frpc

安装后自动建 webui4frpc 系统账号、注册 systemd 单元,数据目录在 /var/lib/webui4frpc。 服务以非 root 运行,并启用 ProtectSystem=full / NoNewPrivileges 等加固项。

Windows

双击 webui4frpc-0.1.4-windows-amd64-setup.exe(ARM 设备用 windows-arm64-setup.exe)。 安装向导会询问监听地址与管理员凭据,完成后:

  • 程序装到 C:\Program Files\webui4frpc
  • 开始菜单生成启动快捷方式与卸载项
  • 自动添加防火墙入站规则

macOS

sudo installer -pkg webui4frpc-0.1.4-darwin-arm64.pkg -target /   # Apple Silicon
sudo installer -pkg webui4frpc-0.1.4-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.4-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

或者用配置文件(推荐,口令不必出现在命令行与进程列表里):

./webui4frpc -config ./config.json

打开 http://127.0.0.1:7500/ ,输入账号密码进入画布。首次使用可到「设置」页一键安装 frpc(或手动指定路径),然后回到画布创建 local / remote 并连线、保存。

参数

参数 默认 说明
-config <workdir>/config.json(或 $W4F_CONFIG) 配置文件路径;文件不存在不算错误
-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 / 通配监听场景用)。

从配置文件启动

除了命令行 flag,也可以用 JSON 配置文件启动。推荐用它承载口令:命令行参数会出现在 ps 输出、systemd 单元文件与 shell history 里,配置文件可以把口令放到只 root 可读的地方。

优先级

命令行 flag  >  配置文件  >  内置默认值
  • 只有显式传入的 flag 才会覆盖配置文件。实现上用 flag.Visit 判断某个 flag 是否真的 被写在命令行上 —— Go 的 flag 包会先把默认值塞进变量,若直接读变量,默认值会无条件 压过配置文件。这是这套逻辑里唯一容易出错的地方,已有回归测试守着 (internal/appconfig 的 TestResolveFileBeatsFlagDefaults)。
  • 配置文件里没写或留空的字段,才回落到内置默认值。
  • -peer 特殊:显式传了 -peer 就用命令行的列表整体替换配置文件里的 peers, 而不是追加 —— 否则在命令行上删掉的对端会被配置文件复活。
  • -join-key 的内置默认是 $W4F_JOIN_KEY;未显式传 flag 时,配置文件里的 join_key 只在环境变量为空时才生效。
  • -config 未传时的查找顺序:$W4F_CONFIG → <workdir>/config.json。 注意 workdir 用的是 flag 值(显式或默认):配置文件必须先能被找到,才谈得上 读取它里面的 workdir。
  • 文件不存在不算错误(回到旧的纯 flag 行为);文件存在但 JSON 解析失败、出现未知键 (例如把 password 拼成 passwd)、或引用了未定义的环境变量,都会立即报错退出, 不会静默忽略 —— 静默回落到 admin/admin 才是真正的安全问题。

字段

字段与 flag 一一对应,全部可选:

字段 对应 flag 说明
addr -addr 监听地址
user -user Basic 认证用户名
password -password Basic 认证口令(推荐用 ${VAR} 或 file:)
workdir -workdir 数据目录
bin -bin 初始 frpc 二进制路径
frpc -frpc worker 默认 frpc 路径
peers -peer 集群对端 host:port 数组
join_key -join-key 集群准入密钥(也接受 joinKey 写法)

口令的两种写法

所有字符串字段都支持这两种展开形式,不只是 password:

{ "password": "${W4F_PASSWORD}" }
{ "password": "file:/etc/webui4frpc/secret/password" }
  • ${VAR} 取环境变量;变量未定义或是空值会报错退出(而不是展开成空口令)。 想写字面量 $ 用 $$。
  • file:/path 只取文件的第一行(去掉行尾换行),适合 chmod 600 的 secrets 文件。 文件读不到会报错退出。
  • 展开后的值不会出现在任何日志里:启动横幅只打印「字段来源」与脱敏摘要 (password=<redacted>、join-key=<redacted>),Values 的 String() 也做了脱敏, 所以连手滑 log.Printf("%v", ...) 都不会漏。

示例

{
  "addr": "0.0.0.0:7500",
  "user": "jianf",
  "password": "file:/etc/webui4frpc/secret/password",
  "workdir": "/var/lib/webui4frpc",
  "frpc": "/usr/local/bin/frpc",
  "peers": ["10.0.0.2:7500", "10.0.0.3:7500"],
  "join_key": "${W4F_JOIN_KEY}"
}

对应的 systemd 单元不再需要把口令写进 ExecStart:

[Service]
User=webui4frpc
Group=webui4frpc
EnvironmentFile=-/etc/webui4frpc/webui4frpc.env
ExecStart=/usr/bin/webui4frpc -config /etc/webui4frpc/config.json
ReadWritePaths=/var/lib/webui4frpc

packaging/common/ 里带了一份可直接抄的单元模板与 config.json 示例。

关于「没配口令」

配置文件里没写 password、命令行也没给时,行为与旧版一致(回落到内置默认值), 但启动日志会打一行明确警告:

WARNING: no password configured (neither -password nor the config file); using the built-in default — change it before exposing this node

负载均衡参数

每节点独立生效;集群页的开关优先于这里的默认值(在页面上选过就以页面为准,重启不丢)。

环境变量 默认 说明
W4F_BALANCE_MODE dry-run off 不参与 / dry-run 只报告 / on 真正释放
W4F_BALANCE_COOLDOWN 600 同一节点两次释放之间的最小秒数
W4F_BALANCE_INTERVAL 10 检查间隔(秒)

释放会停掉该转发的 frpc worker,其上的在途连接会断一次,由新节点认领后恢复; 因此默认是 dry-run。集群页的开关走 write 级权限。

启动行为

节点启动时的集群引导优先级:

  1. 缓存拓扑优先:读取持久化的 ClusterPeers(宕机前最后令牌周期保存的对端列表),逐个尝试 rejoin
  2. -peer 引导:无缓存拓扑时,用 -peer + -join-key 加入指定对端(10s 重试、5min 超时)
  3. 新建集群:无缓存无 -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 令牌环快照
GET /cluster/balance read 本节点负载均衡视图(只读,不变更状态)
POST /cluster/balance/mode write 切换本节点负载均衡模式(持久化)
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 归属)、负载均衡(本机份额与释放计划)、增量事件日志。

负载均衡(自导式释放)

为什么不是 leader 统一调度:一节点的负载信号是它持有的转发数,而这个数只在 它自己的拓扑条目消失时才下降 —— 条目是本地簿记。所以 leader 从外面只能看见 失衡,无法消除它;要消除就得伸手改另一个节点的簿记,而这正是这个环到处都在避免 的跨节点写(撤销、重启同样都是定向往 owner)。

于是规则由每个节点对自己的转发本地执行:

当  持有转发数 >= 2  且  持有转发数 > ceil(总转发数 / 存活节点数)
则  撤销其中一条 → 删除条目 → 以启用状态重新发布到环 → 由最闲节点认领
  • 自限:撤到平摊值即停。3 条分 3 台时,持有 2 条的那台卸一条后变成 1 条, 1 > 1 不成立 → 停手。没有这个上界,owner 会把转发全部卸光。
  • 不会卸空:低于下限 2 条永不撤销。
  • 不碰已停用的转发:停用条目的 entry 是用户「停用」决定的载体,释放它等于把 一条已停的转发当活干交给别人。
  • 释放 ≠ 停止:用户停止保留条目并标记链路为停用;均衡释放删除条目且链路保持 启用。两者不能混用 —— 复用停止的话,每个节点的重复认领守卫都会拒绝这条已是「有主」 的转发,它会被永久搁死在刚放弃它的那台机器上。

三种模式(集群页每节点一个开关,也可用 W4F_BALANCE_MODE 设默认值):

模式 行为
off 不参与均衡
dry-run 只报告「将释放哪条」,不动任何东西(默认)
on 真正释放

只读接口 GET /cluster/balance 返回本节点视角:selfForwards / share / totalForwards / aliveNodes / selfOverShare、wouldRelease、releasing、 blockedBy(为何暂不动的机器原因)、mode / modeSource / canControl。 该接口不改变任何状态 —— 反复轮询不会启动、延迟或取消一次释放。

测试

# 后端
go test ./...

# 单包(负载均衡规则与状态机)
go test ./internal/cluster/ -run TestShare -v

# 前端类型检查
cd web && npm run build
Description
No description provided
Readme 5.6 MiB
Languages
Go 66.1%
Vue 23.2%
Shell 4.1%
TypeScript 3.1%
CSS 1.9%
Other 1.5%