feat(cert,scripts,docs): Linux 原生安装包与证书申请三处修复(DNS 解析器 / Buypass 下线 / ZeroSSL 报错与密钥脱敏)(v1.2.13)
CI / verify (push) Canceled after 0s

- Linux 安装包:scripts/build-linux.ps1 交叉编译出 fonu-<版本>-linux-{amd64,arm64}.tar.gz(二进制 + migrations + systemd 安装脚本 + 运行时资源 + sha256),scripts/fonu-install.sh 负责装机(/opt/fonu、/var/lib/fonu、/etc/fonu/fonu.env 首次生成随机会话密钥)并新增 --reset-admin;装完直接打印初始管理员口令

- 证书 DNS-01:lego 默认读系统 resolv.conf(容器里是 Docker 的 127.0.0.11,不返回权威 NS)导致 could not determine authoritative nameservers;现显式传 AddRecursiveNameservers,默认 223.5.5.5/1.1.1.1/8.8.8.8,设置项 acme_dns_resolvers 可覆盖(system = 回到系统解析器)

- Buypass 已停止签发(2025-10-15)并终止服务(2026-04-15):从可选 CA 移除,历史记录仍可展示,直接提交会被拦下并给出中文原因

- ZeroSSL:422 时带出 error.type/error.code(非 JSON 退回截断正文);同时修掉网络错误把含 access_key 的完整 URL 写进任务日志与 app.log 的密钥泄露

- 测试:解析器列表解析 3 例 + ZeroSSL EAB 4 例(含不泄露密钥断言);gofmt / go build / go vet / go test 全绿
This commit is contained in:
2026-09-27 10:10:15 +08:00
parent 2236259a52
commit dda0eb876e
18 changed files with 1353 additions and 32 deletions
+429
View File
@@ -0,0 +1,429 @@
# Fonu 品牌升级 / 改名方案
> 日期:2026-09-25 对象:Fonu 1.0.2(VERSION 1.2.12)
> 方法:把 "Fonu / fonu / FONU_" 在仓库里**实际出现的位置逐个查出来**(下文带 `文件:行` 的都是读代码得到的),再按「改动代价 × 破坏性」分层给方案。
> 结论速览:**能做,而且没有"改不动"的地方;但"全面改名"绝不等价于全局替换——有 3 处一动就会伤到存量用户(数据库文件名、环境变量前缀、远端 agent 凭据路径)。推荐策略 B:分层渐进 + 兼容层,一个版本后清理兼容代码。**
---
## 〇、一句话回答
| 问题 | 回答 |
| --- | --- |
| 能不能换名? | 能。所有品牌落点都是可枚举的文本/路径/常量,没有第三方注册死锁(GitHub 仓库名可改、镜像名可切、CLI 名随二进制)。 |
| 能不能"全面"? | 能,但要分 5 层做。**展示层(用户看见的字)改起来是半天的事,存储层与契约层(数据文件名、环境变量、远端 agent 路径)必须带兼容层**,否则老用户升级后会出现「数据像丢了」「容器起来了但库是空的」「VPS 上的 agent 失联」。 |
| 改名量级 | 实测 `Fonu|fonu|FONU` 至少 1912 处、120+ 个文件(分页上限,实际更多);其中约 2/3 是 CSS 变量(`--fonu-*`)、Go import 前缀、文档正文这类**机械替换**,真正的决策点只有 5 个(见 §二)。 |
---
## 一、品牌落点盘点(按层,带实测证据)
### 第 1 层:展示层(用户能看见的字与图)
| 落点 | 位置 | 说明 |
| --- | --- | --- |
| 浏览器标题 | `web/index.html` `<title>Fonu</title>` | 建议与侧栏名统一 |
| 侧栏品牌区 | `web/src/layouts/AppLayout.vue:15,17,34` | logo `alt="Fonu"`、`brand__name` 文字、页脚品牌名;`:132` 引 `../assets/brand/logo.png` |
| 品牌标语 | `AppLayout.vue:18` 「让 NAS 访问更简单」 | 换名时建议一并重写(新名 + 新一句话定位) |
| 视觉资产 | `web/src/assets/brand/logo.png`、`web/public/favicon.png`、`web/assets/image/logo.png`、`web/assets/image/banner.png` | 图片需重做;`web/assets/image/*` 由 Dockerfile `COPY web/assets/image /app/web/assets/image` 进镜像 |
| 登录页 | `web/src/views/LoginView.vue` | 品牌露出最集中的页面之一 |
| 公共组件 | `web/src/components/{PageHeader,EmptyState,LoadError,StatusBadge,StatCard,FonuCard}.vue` | 前 5 个是文案/样式里的品牌残留,`FonuCard.vue` **文件名本身含品牌名**(含 import 该组件的多处 view) |
| 前端常量 | `web/src/constants/app.ts:1` `GITHUB_REPO = 'O96u/Fonu'` | 对外展示的仓库地址,改名必须同步(见 §六) |
| 通知消息 | `internal/notify/format.go`、`internal/notify/{service,store,push}.go` | 邮件/推送正文里的品牌前缀,用户直接在手机上看到 |
| 公开状态页 | `internal/api/status_page_handler.go`、`status_handler.go`(各 4~8 处) | `/status` 公开页标题、页脚品牌 |
| CSS 变量 | `web/src/styles/variables.css:2` 起 `--fonu-brand`、`--fonu-brand-soft`…(全仓 51 处) | **纯机械替换**,但波及所有 `.vue` 样式;建议一次性做且用 `replace_all` |
| 本地偏好 key | `web/src/composables/useTheme.ts:4` `themeKey = 'fonu-theme'` | 改名后老用户主题偏好重置(可一行兼容,见 §五.4) |
### 第 2 层:交付 / 部署层(用户拿到的东西叫什么)
| 落点 | 位置 | 风险 |
| --- | --- | --- |
| 主程序二进制名 | `Dockerfile`:构建产物 `/fonu` → `COPY --from=go-builder /fonu /app/fonu`、`CMD ["/app/fonu"]`;发布物 `fonu-1.1.0-linux-amd64` | 中:换名即换文件名,用户脚本里的路径要跟着改 |
| agent 交叉编译名 | `Dockerfile`:`go build -o internal/frp/agentbin/fonu-agent-linux-{amd64,arm64}` | **高**:与 `internal/frp/agentbin/embed.go` 的 `//go:embed fonu-agent-linux-amd64` 强耦合,改一处必须同批改另一处,否则编译期直接失败 |
| Docker 编排 | `docker-compose.yml`、`.host.yml`、`.hub.yml`:service `fonu`、`container_name: fonu`、`image: fonu:local`、volume `fonu-data` | **高**:volume 名一变,`docker compose up` 会建**新空 volume**,用户会认为「数据丢了」;必须写清迁移命令或显式 pin `name:` |
| 镜像仓库名 | `.github/workflows/docker-release.yml:63,94` `${DOCKERHUB_USERNAME}/fonu:<ver>-<arch>` | Docker Hub **不支持仓库改名**(只能新建),见 §六 |
| 前端 embed 路径 | 根 `.gitignore` 的 `cmd/fonu/web/dist/`、`.github/workflows/ci.yml:19,42,43`、`//go:embed all:web/dist` 所在 `cmd/fonu/main.go` | 中:目录改名会牵动 CI、构建脚本、embed 指令、Dockerfile |
| agent 安装布局 | `scripts/agent-install.sh`:`PREFIX=/usr/local/bin/fonu-agent`、`CONF_DIR=/etc/fonu-agent`、`UNIT=/etc/systemd/system/fonu-agent.service`、`/var/lib/fonu-agent/certs` | **高**:涉及已在用户 VPS 上运行的服务,升级路径必须兼容(见 §五.5) |
| 环境变量 | `internal/config/config.go:24,29,31~38`(`FONU_DATA_DIR`/`FONU_LISTEN`/`FONU_SESSION_SECRET`/`FONU_NGINX_*`/`FONU_FRPC_BIN`/`FONU_FRP_PID`)+ `Dockerfile` 的 `ENV` 块 + `docker-compose*.yml` + `docs/DEPLOYMENT.md`(大量) | **高**:前缀一改,老用户的 `.env` / compose / `-e` 全部静默失效 → 容器用默认值起来(例如数据目录跑回 `/data` 之外、会话密钥回到 `change-me-in-production`) |
### 第 3 层:代码标识层(改了不影响运行语义,但波及面最大)
| 落点 | 位置 | 说明 |
| --- | --- | --- |
| Go module path | `go.mod:1` `module github.com/fonu/fonu` | **每个 .go 文件的 import 块**(`internal/api` 20 个文件、`internal/nginx`、`internal/frp`…);同时 `Dockerfile` 的 `-ldflags "-X github.com/fonu/fonu/internal/version.Version=..."` 必须同步改,否则版本号注入静默失效(二进制显示 `dev`) |
| 前端包名 | `web/package.json:2` `"name": "fonu-web"` | 机械替换 |
| 内嵌 agent 资产名 | `internal/frp/agentbin/embed.go` | 见上,与 Dockerfile 成对出现 |
| 文档 | `README.md`、`CHANGELOG.md`、`docs/*.md`(20+ 篇,其中 `DEPLOYMENT.md` 56 处、`frps服务端管理方案.md` 52 处) | 量大但不危险;建议随代码同批改,避免文档与实现不一致 |
### 第 4 层:运行期契约层(跨进程/跨版本,最容易漏)
| 落点 | 位置 | 说明 |
| --- | --- | --- |
| 会话 Cookie 名 | `internal/api/middleware.go:18` `r.Cookie("fonu_session")` | 改名 = **所有已登录用户掉线**(可接受,但要在发布说明写明) |
| agent 凭据文件 | `internal/frp/agent_installer.go:35,36,168` + `:145`:服务端通过 SSH 在 VPS 上 `sed` 读 `/etc/fonu-agent/agent.env` 的 `FONU_AGENT_TOKEN` | **服务端与 agent 必须同批发布**:服务端换了路径/变量名,而用户 VPS 上还是旧 agent(或反之),「Agent 探测」会直接失败 |
| agent 自述标识 | `internal/agent/server.go:213,221` `{"ok":true,"service":"fonu-agent"}` | 属于对外可见的 API 响应体;若有任何前端/脚本按 `service` 字段判断,改名前要一起查 |
| agent 落盘目录 | `internal/agent/server.go:133,145`:`/var/lib/fonu-agent/certs`、`/var/lib/fonu-agent` | 与安装脚本一致 |
| 远端 systemd unit 描述 | `internal/agent/server.go:344` `Description=frps service (managed by fonu-agent)` | 远端残留可见文本 |
| 远端 nginx 探针/标记 | `internal/agent/server.go:674-675`:写 `fonu-probe.conf`、文件头 `# Managed by fonu-agent. Do not edit.`;`:692` 已有「清理历史残留 `fonu-*.conf`」逻辑 | **改名最容易踩的隐性坑**:老 VPS 上留着旧品牌标记的片段,新版本若只认新标记,会把它们判成「未知文件」→ 漂移检测误报或残留永不清理 |
| 远端 frp 安装与漂移 | `internal/frp/ssh_installer.go`、`internal/frp/route_drift.go`(各 4 / 2 处) | 同上:判定「这是我管理的文件」的依据里含品牌标记 |
### 第 5 层:数据层(最危险,改错就是"丢数据")
| 落点 | 位置 | 说明 |
| --- | --- | --- |
| 数据库文件名 | `internal/config/config.go:49-51` `DBPath() = <DataDir>/fonu.db` | **一改,程序找不到库 → 建一个空库 → 用户以为数据没了**。必须带「探测旧文件 + 原子重命名」 |
| 数据目录默认值 | `config.go:126` `defaultDataDir()`:`/data` 或 `<cwd>/.data` | ✅ **好消息:不含品牌名,无需迁移** |
| 各子目录 | `config.go` 的 `NginxDir/LogsDir/CertsDir/FrpDir`…(`nginx`/`logs`/`certs`/`frp`/`ssh_known_hosts`) | ✅ 也都不含品牌名,只有 DB 文件名带品牌 |
| 迁移脚本 | `migrations/023_frp_server_agent.sql`(1 处品牌) | SQL 迁移一般不需要改(库文件名才是关键路径) |
> 盘点的意义:**品牌名"长"在 5 个不同性质的地方**——字、路径、常量、协议、文件名。前两类可以随便改,后三类改之前要先想好兼容。
---
## 二、三个必须先解决的暗礁
1. **数据库文件名 `fonu.db`(`internal/config/config.go:50`)**
裸改 = 静默新建空库。做法:启动时若「新名库不存在」且「`fonu.db` 存在」→ 同目录 `os.Rename` 到新名(原子、可回滚),失败则**继续用旧路径并在日志/界面提示**,绝不新建第二份库。Windows 上文件被占用时 rename 会失败,所以「失败即回退到旧路径」这条兜底不是可选项。
2. **环境变量前缀 `FONU_*`(`internal/config/config.go:24,29,31-38`)**
裸改 = 老部署的 `FONU_SESSION_SECRET` 失效 → 会话密钥回到公开默认值 `change-me-in-production`(**这是安全问题,不只是体验问题**:拿到默认密钥可伪造管理员会话、解密库里凭据,见 `docs/DEPLOYMENT.md` 对该密钥的说明)。
做法:`envOr` 升级为「新前缀 → 旧前缀 → 默认值」三档读取,命中旧前缀时启动日志给一条明确的迁移提示(含新旧变量名对照),至少保留一个 stable 版本后再删旧前缀。**尤其 `FONU_SESSION_SECRET` 与 `FONU_DATA_DIR` 必须双读。**
3. **远端 agent 的凭据路径与品牌标记(`agent_installer.go:35,36,145,168`;`agent/server.go:674,692`)**
服务端是**通过 SSH 去读用户 VPS 上的文件**来判断 agent 状态的。服务端与 agent 不同批发布时:
- 探测读旧路径、agent 写新路径 → 「Agent 探测」永远显示未配置;
- nginx 探针/片段标记改名 → 旧文件既不被清理也不被识别,漂移检测出现假阳性。
做法:探测端**两个路径都试**(新路径优先,旧路径兜底);`agent` 二进制**同时接受新旧 env 变量名**;生成器把新旧品牌标记**都视为"自己生成的文件"**(清理/漂移判定同时匹配),并在 agent 版本号里体现本次变更(本仓既有约定:agent 行为变更需递增 `AgentVersion` 并重编内嵌双架构二进制)。
---
## 三、三种策略与推荐
| | 策略 A:只换皮 | **策略 B:分层渐进 + 兼容层(推荐)** | 策略 C:一次性硬切 |
| --- | --- | --- | --- |
| 范围 | 只做第 1 层(文案/图/CSS 变量/通知) | 第 1→2→4→5→3 层按阶段推进,关键处留兼容 | 5 层全改,不留兼容 |
| 代码净度 | 低(`fonu.db`、`fonu-agent`、`FONU_*` 仍是旧名) | 中:过渡期有双读分支,1~2 个版本后清理 | 高 |
| 存量用户 | 无感 | 无感(或仅掉一次登录态) | **有损**:需手工迁移数据目录/重建 volume/重装 agent |
| 适合 | 只想换个门面、用户量很小 | 已有真实用户、有 Docker 与 VPS 两种部署形态(本仓现状) | 尚未发布 / 可强制清退老版本 |
| 代价 | 半天量级,零风险 | 机械替换 + 5 个兼容点 + 一轮双形态回归 | 改动最少但迁移成本转嫁给用户 |
**推荐 B**,理由:本仓已有 Docker(`docker-compose*.yml`)、host 直装、VPS agent 三种落地面,且 `docs/DEPLOYMENT.md` 里明确把 `FONU_SESSION_SECRET` 当作安全关键项——这些决定了「裸改」的代价远高于「留兼容层」。
---
## 四、推荐执行路线(五阶段)
> 每阶段的验证沿用本仓既有约定:`go build ./... && go vet ./... && go test ./...` + `cd web && npm run typecheck && npm run build`;涉及 agent 的改动递增 `AgentVersion` 并重编内嵌二进制;用户明确要求提交时,才同批更新 `CHANGELOG.md` 与 `VERSION`。
### 阶段 0:定名与冻结(不写代码)
- 定下新品牌名与英文小写标识(CLI/包名/目录用)、显示名(首字母大写形态)。
- 检查占用:域名、GitHub org/repo、Docker Hub 命名空间、唯一包名(如 npm/PyPI 若以后要用)、以及**中文语境下的商标检索**。
- 决定 GitHub 与 Docker Hub 的走向(§六),因为 `web/src/constants/app.ts:1` 与版本检查 `web/src/utils/version.ts:37` 直接依赖它。
- 产出:`旧名 → 新名` 对照表(含:显示名、小写标识、二进制名、镜像名、容器名、env 前缀、cookie 名、DB 文件名、agent 目录名)。
### 阶段 1:展示层(零风险,先拿到"看着像新品牌"的反馈)
- 改:`web/index.html` 标题、`AppLayout.vue:15,17,18,34`、`LoginView.vue`、公共组件(含 `FonuCard.vue` 重命名)、`web/src/constants/app.ts:1`、`internal/notify/format.go`、`internal/api/status_page_handler.go`/`status_handler.go` 的品牌文案。
- 换图:`web/src/assets/brand/logo.png`、`web/public/favicon.png`、`web/assets/image/{logo,banner}.png`。
- CSS 变量 `--fonu-*` → `--<new>-*`(`variables.css` 定义 + 全部 `.vue` 引用,用 `replace_all` 一次完成)。
- 验证:`npm run build` 通过 + 前端手点一遍侧栏/登录页/设置页/公开状态页。
- 回滚:纯文案与样式,`git revert` 即可。
### 阶段 2:交付层(二进制、镜像、compose、发布物)
- 二进制 `fonu` → 新名;`docker-compose*.yml` 的 service/容器名/image/volume;`.github/workflows/docker-release.yml:63,94` 的镜像路径。
- **volume 必须处理**:在 compose 里显式声明 `volumes: { <new>-data: { name: <new>-data } }` 并在 `docs/DEPLOYMENT.md` 写「老用户迁移三行命令」(旧 volume 挂到新名,或 `docker volume create` + 复制)。
- 老二进制名建议保留一个版本(同镜像内留符号链接,或发布说明里给 rename 命令),避免用户脚本直接报错。
- 验证:本地 `docker compose up`,用**旧 volume** 起新容器,确认库被正确打开(这是本阶段的验收核心,不是"能起")。
- 回滚:compose 文件与镜像名回退即可;volume 命名要事先确定,避免中途再改一次。
### 阶段 3:存储层与契约层(暗礁集中区,必须带兼容)
- `config.go`:`DBPath()` 换新名 + **旧库自动接管**(§二.1);`envOr` 支持旧前缀兜底 + 启动迁移提示(§二.2)。
- `middleware.go:18` cookie 名:可选做「新名 cookie 优先、旧名兜底」读一次,或直接换名并在发布说明写「需重新登录」。
- `useTheme.ts:4`:读旧 key 回退 + 写回新 key(三行代码,避免用户主题偏好被重置)。
- agent 侧:`agent-install.sh`(`/usr/local/bin`、`/etc`、`/var/lib`、systemd unit 名)、`internal/agent/server.go:133,145,213,221,344,674,692`、`internal/frp/agent_installer.go:35,36,145,168`、`ssh_installer.go`、`route_drift.go`。
- 安装脚本要能**识别并接管已安装的旧 agent**(停旧 unit、装新 unit、保留/迁移 `/etc/fonu-agent` 的 env 文件),而不是留下两套服务互相抢。
- 探测与漂移判定:新旧路径/标记**都算自己人**,至少一个版本。
- 验证(关键):造一份**旧版数据目录**,用新二进制启动 → 确认打开的是旧库且数据完整;再用**旧版 agent** 对**新版服务端**跑一次「Agent 探测 / 安装 / 日志」;`go test ./...` 通过(新增的 DB 接管与 env 双读建议各补一个纯函数单测,参照本仓 `maskFRPSSecrets` 那类测试写法)。
- 回滚:DB rename 是单向的(必要时提供反向重命名说明);env 双读让回滚无感。
### 阶段 4:代码标识层(机械但波及全仓)
- `go.mod:1` module path → 新路径;全仓 import 替换(`gofmt`/`goimports` 后 `go build ./...` 兜底)。
- **同步点**:`Dockerfile` 的 `-ldflags "-X github.com/fonu/fonu/internal/version.Version=..."`(漏改则版本号变 `dev`,且不报错);`internal/frp/agentbin/embed.go` 与 Dockerfile 的 agent 输出名;`web/package.json:2`;`cmd/fonu/` → `cmd/<new>/`(连带 `.gitignore`、`ci.yml:19,42,43`、embed 指令)。
- 建议与阶段 2/3 分开发布(单独一次提交),便于「编译失败」时快速定位。
- 验证:`go build ./... && go vet ./... && go test ./...`;构建后确认 `GET /api/version`(或设置页)显示真实版本号而非 `dev`。
### 阶段 5:清理(1~2 个版本后)
- 删除旧 env 前缀兜底、旧 DB 文件名探测、旧 cookie/localStorage key、旧 agent 路径与标记、旧预先/镜像别名。
- 用 §九 的自查清单跑一次全仓 grep,确认只剩新名。
---
## 五、兼容层设计要点(可照做)
1. **环境变量**:`envOr` → `envOrLayered(newKey, oldKey, default)`;命中旧 key 时启动日志输出一次「检测到旧变量 `FONU_DATA_DIR`,已生效;请迁移到 `<NEW>_DATA_DIR`」。优先级:新 > 旧 > 默认,**不要**用「旧覆盖新」。
2. **数据库接管**:判断顺序 = 新库存在? → 用新库;否则旧库存在? → rename 后使用(失败则直接用旧路径 + 明确提示);否则建新库。全程不改动用户其它文件。
3. **Docker volume**:`name:` 显式固定新名;给出一条从旧 volume 复制的命令;**不要**靠 compose 自动改名。
4. **前端本地状态**:`localStorage.getItem('fonu-theme') ?? localStorage.getItem('<new>-theme')`,读到旧值时写回新 key。
5. **agent 平滑升级**:
- agent 二进制双读新旧 env 名(`FONU_AGENT_TOKEN` 与 `<NEW>_AGENT_TOKEN`);
- 安装脚本 `systemctl disable --now fonu-agent` → 装新 unit → 迁移 `/etc/fonu-agent/agent.env` → 启动新 unit;
- 服务端探测命令**两个路径并联**(`test -s A || test -s B`),避免版本错配期间误报;
- 远端文件识别同时匹配新旧标记;`server.go:692` 的「历史残留清理」逻辑扩成「新旧品牌前缀都清」。
6. **凭据与密钥**:品牌改名**不应触碰**加密数据本身(`secret.Box` 加密的 DDNS/FRP/SMTP 凭据与品牌无关),所以只要 `FONU_SESSION_SECRET` 双读做好,老用户的凭据继续可用——这也是「不要顺手改密钥派生」的理由。
---
## 六、平台与外部身份
| 项 | 现状 | 建议做法 |
| --- | --- | --- |
| GitHub 仓库 | `web/src/constants/app.ts:1` `O96u/Fonu`;`web/src/utils/version.ts:37` 用 `api.github.com/repos/${GITHUB_REPO}/releases/latest` 做版本检查 | GitHub **支持仓库改名并保留旧地址重定向(含 API 与 release 资产下载)**,但重定向会被「别人用旧名新建仓库」永久打断;对自用/小众项目风险低,仍建议:改名后**保留旧仓库名不被占用**(或在旧名新建一个空仓库做说明页——注意这恰好会打断重定向,二者只能选一)。改名后必须回归一次版本检查(`version.ts` 是前端直连 GitHub API)。 |
| Docker Hub | `.github/workflows/docker-release.yml:63,94` `${DOCKERHUB_USERNAME}/fonu` | Docker Hub **不支持仓库改名**,只能新建仓库并重推标签;老用户 `docker pull` 旧名会继续拿到旧标签(不会自动指向新镜像,也不会报错)→ 发布说明里给出新镜像地址 + 迁移命令。 |
| 域名 / 文档站 | 仓库内无硬编码站点域名 | 若已注册 `fonu.*` 域名,改名后建议保留旧域名 301 到新域名(同 GitHub 重定向的思路)。 |
| 文档 | 20+ 篇 md(`DEPLOYMENT.md` 56 处、`frps服务端管理方案.md` 52 处…) | 与代码同批改;`docs/brand-rename-plan.md`(本文)建议保留,作为改名决策记录。 |
| 发布物命名 | 根目录 `fonu-1.1.0-linux-amd64`、`fonu-1.1.0-src.zip`、`fonu-image-build4.log` 等 | 历史产物不必改名;新产物按新名。 |
---
## 七、新名候选与自查
**命名约束(硬性)**:
- 小写标识:仅 `[a-z0-9-]`,2~8 字符,无连字符歧义(`<new>-agent`、`<new>.db`、`--<new>-brand`、`<NEW>_DATA_DIR` 都要好看);
- 不与现有工具重名(`frp`、`nginx`、`caddy`、`tunnel` 等已占用词避免单独使用);
- 域名 / GitHub org / Docker Hub / npm 包名四处可注册;
- 中文可读(本项目用户群中文语境,"念出来不别扭"很重要);
- 商标自查(至少本国 + 主要市场)。
**候选清单**(`标识` 一列就是命令行、包名、`<标识>-agent`、`<标识>.db`、`<IDENT>_DATA_DIR` 要用的形态;每一项在落地前都要走 §七.3 的四处自查,下表「备注」只标注**已知**的同名存在,不代表未被占用)
#### A. 通路 / 门 / 桥(英文语义直白,技术用户第一眼就懂)
| 显示名 | 标识 | 中文名建议 | 寓意与适配 |
| --- | --- | --- | --- |
| Portway | `portway` | 通途 | port(端口)+ way(通路)——产品做的就是"把端口接出去",语义与技术完全对齐;`portway-agent`、`portway.db` 读感都好 |
| Viaduct | `viaduct` | 虹桥 | 高架桥:在两个互不相通的网络之间架一条通路;画面感强,适合做几何型 logo |
| Causeway | `causeway` | 长堤 | 跨水而过的路——跨过 NAT/"内网之海";8 字符是上限,仍可接受 |
| Underpass | `underpass` | 潜通 | 字面"从下面穿过去",暗合 NAT 打洞的本意;比 Underpass 更贴的同类还有 Overpass,但后者已被 OSM 的查询 API 广泛占用 |
| Thruway | `thruway` | 直通 | thru 带"穿透"的俚语味,拼写短促;注意美式拼写,正式英文里少见 |
| Ponto | `ponto` | 蓬托 | 葡萄牙语/世界语"桥",5 字符、短促好念;同名存在较多(银行、设计工具),必须自查 |
#### B. 渡 / 舟 / 引渡(中文意象最贴产品,拼音做标识)
| 显示名 | 标识 | 中文名 | 寓意与适配 |
| --- | --- | --- | --- |
| Dukou | `dukou` | 渡口 | **语义完全命中**:把内网的船渡到公网对岸;中文用户一看就懂、一说就记住;`dukou` 5 字符,命令行友好 |
| Zhandao | `zhandao` | 栈道 | 在无路可走的险处架出一条路——正是 NAT 之后的处境;名字冷门,检索不打架 |
| Yongdao | `yongdao` | 甬道 | 连接两端的地下通道;比"隧道"更雅,避开与 tunnel 这类通用词的碰撞 |
| Changdu | `changdu` | 长渡 | "长期摆渡"的意味,适合强调常驻服务;缺点是"长渡"作为词不常见 |
| Yinqiao | `yinqiao` | 引桥 | 从匝道引上主桥的那一段——很像"从内网引到出口";亲切、口语化 |
> 说明:中文意象类在中文用户群里记忆点最强,代价是国际展示偏窄;如果以后要面向海外,建议「英文标识 + 中文名」成对使用(例如 `dukou` / 渡口),而不是只取其一。
#### C. 延续 Fonu 语音(保留 Fo 音,让改名读起来像"升级"而不是"换了个软件")
| 显示名 | 标识 | 说明 |
| --- | --- | --- |
| Fonara | `fonara` | 保留 `Fo` 音 + 自造后缀,老用户不会觉得陌生;自造词在域名/商标上的可得性通常好于现成词 |
| Fonix | `fonix` | 与 Phoenix(凤凰)谐音,正好呼应"品牌涅槃升级";注意存在 Fönix 等同名公司,需自查 |
| Fonia | `fonia` | 希腊语系词根常与"声音/传输"相关(telefonia),5 字符干净;要确认不与电信类品牌混淆 |
| Fonli | `fonli` | 最短的延续方案,`fonli-agent` / `FONLI_DATA_DIR` 都短;缺点是语义联想弱,需要靠视觉与标语补 |
#### D. 自造短词(域名与商标可得性最好,语义靠后续塑造)
| 显示名 | 标识 | 中文名建议 | 备注 |
| --- | --- | --- | --- |
| Nuvio | `nuvio` | 努维 | 发音圆润、无强语义包袱,适合做纯品牌名 |
| Tando | `tando` | 坦途 | 与"坦途"天然谐音——自造词的语义可由中文名反向赋义,这个配对意外地贴 |
| Kaimu | `kaimu` | 开目 | 意为"把门打开",发音柔和 |
| Runa | `runa` | 鲁纳 | 短、好拼、国际化;注意与 Runa 相关的既有品牌 |
| Sairo | `sairo` | 赛罗 | 读音有速度感,适合强调链路性能 |
| Airlock | `airlock` | 气闸 | 不算自造词,但画面感极强:内网是舱内、公网是舱外,穿透就是"过气闸";独特、好画 logo,且不与 frp/nginx/tunnel 语义重叠 |
#### D 组补充:再给 10 个自造短词
**D-1 音节谐音型** —— 读音贴合一个中文词,中文用户不需要解释就记住,中文名与标识互相印证:
| 显示名 | 标识 | 中文名 | 说明 |
| --- | --- | --- | --- |
| Kaitu | `kaitu` | 开通 | 读音近似「开通」,含义直接落在"把内网服务开通到公网";5 字符,`kaitu-agent`、`KAITU_DATA_DIR` 都短 |
| Kailu | `kailu` | 开路 | 与「开路」完全同音——在 NAT 后面开出一条路,痛点隐喻最强;代价是内蒙古开鲁县同名,检索会混入地名 |
| Toulu | `toulu` | 透路 | 「透」(穿透)+「路」(通路),两个核心动作各取一字;`toulu` 生僻,检索最干净 |
| Chuantu | `chuantu` | 穿途 | 「穿」+「途」,气质偏文学;8 字符正好卡在硬上限,`chuantu-agent` 偏长,适合只在品牌层使用、标识再截短 |
**D-2 纯自造型** —— 无既有语义包袱,靠 logo 与标语赋义,域名与商标的可得性通常更好:
| 显示名 | 标识 | 中文名建议 | 说明 |
| --- | --- | --- | --- |
| Kuvo | `kuvo` | 库沃 | 4 字符、K/V 音干脆,读起来就像产品名;注意先锋 DJ 有同名社区平台 KUVO,必须自查 |
| Kelo | `kelo` | 松洛 | 芬兰语 kelo 指北极圈的站桩枯松(在恶劣环境里站住的意思),冷僻、科技圈少见 |
| Veko | `veko` | 维克 | 纯自造,音形短促,适合做"快"的联想 |
| Yomo | `yomo` | 悠陌 | 发音柔和,中文名可往"悠然可达"方向塑造 |
| Tovo | `tovo` | 途沃 | 首音节与「途」谐形,自带一点通路感,便于往中文语义靠 |
| Kivo | `kivo` | 启沃 | 首音节与「启」(开启)谐形,在图标上可以做"开门"的暗示 |
> 两条路线的取舍:**D-1 在中文本地传播最省力**(不用解释就懂),**D-2 在域名与商标上更干净**(生僻意味着少撞)。用户群以中文 NAS 玩家为主时 D-1 沟通成本最低;若之后要同时面向海外,D-2 + 一个中文名是更稳的组合。
#### E. 「本地 / 自家」语义(名字自带"服务跑在本地"的意思)
**E-1 拉丁语与概念型**("就地、内部、家宅"的直译,气质克制,适合技术向调性)
| 显示名 | 标识 | 中文名建议 | 说明 |
| --- | --- | --- | --- |
| Insitu | `insitu` | 就地 | 拉丁语 in situ = "在原位、就地"——服务不往外搬,就在它原本那台机器上运行。6 字符,冷僻,检索最干净 |
| Intra | `intra` | 内域 | 拉丁语"内部",直接指向内网/局域网;注意 Google Jigsaw 有同名的反审查 DNS 应用,需自查 |
| Domus | `domus` | 宅邸 | 拉丁语"家宅",数据与服务都收在自己的房子里;Domus 是欧美建筑媒体的常用名 |
| Localis | `localis` | 本地 | local 的拉丁语源,语义最直白;缺点是偏长(7 字符),且与英国一家同名智库撞车 |
**E-2 跨语言的"家"**(一个字就说清"自己的地方",且大多冷僻)
| 显示名 | 标识 | 中文名建议 | 说明 |
| --- | --- | --- | --- |
| Kodu | `kodu` | 科杜 | 爱沙尼亚语"家";4 字符、极冷僻,念起来干净利落 |
| Sumika | `sumika` | 住屋 | 日语「住処」(すみか)= 栖息之所;6 字符,画面温和,科技圈少见 |
| Jitaku | `jitaku` | 自宅 | 日语「自宅」,含义精确到"自己的房子",与 self-hosted 一一对应 |
| Uchi | `uchi` | 屋内 | 日语「内 / うち」同时是"内部"和"家";缺点是与美国连锁日料品牌 Uchi 同名 |
| Hearth | `hearth` | 炉边 | 英语"炉边",是"家"最温暖的意象;缺点是撞《炉石传说》的简称 Hearth |
**E-3 中文拼音直觉型**(中文用户零解释成本,代价是通用词难注册)
| 显示名 | 标识 | 中文名 | 说明 |
| --- | --- | --- | --- |
| Benji | `benji` | 本机 | 拼音「本机」完全对应,语义最准;缺点是与英文人名 Benji 撞车,检索会被淹没 |
| Bendi | `bendi` | 本地 | 最直白的一个;正因为通用,作为商品名与商标的注册难度最大 |
| Jiudi | `jiudi` | 就地 | 拼音「就地」,是 E-1 里 `insitu` 的中文同义形态,两者可以互为备选 |
| Wojia | `wojia` | 我家 | 亲切、有归属感,一句"服务就在我家跑着"把定位说完;注意与房产类品牌谐音 |
| Yuannei | `yuannei` | 院内 | "自家院子里那台机器"——NAS 用户共情很强;7 字符偏长 |
| Anwo | `anwo` | 安窝 | "给服务安个窝",读音短、意象活泼,适合偏年轻的产品调性 |
> 两点提醒:
> 1. **`homelab` / `self-hosted` / `on-prem` 这几个词恰好描述你的产品,但它们是行业通用术语,不能当商品名**——建议放进标语(例如「为 homelab 而生」),品牌名另取。
> 2. "本地"语义与定位最贴,也最容易撞现成词:`bendi`、`benji`、`localis` 这类直白形态被占的概率,明显高于 `insitu`、`kodu`、`sumika` 这类冷僻形态。走这条语义线时,优先在冷僻形态里挑。
**这条语义线上的首选三个**
1. **`insitu` / 就地** —— 语义精确(就地运行、不外迁)、拉丁出处让名字有质感、且检索干净,是"本地服务"这层意思里最不像拼凑的一个。
2. **`jitaku` / 自宅** —— self-hosted 的日文对应词,含义零歧义,短而冷门,写进 `<标识>-agent`、`<标识>.db` 也好看。
3. **`kodu` / 家** —— 4 字符、跨语言都能念,留白最多,便于日后把它讲成"你自己的服务空间"。
#### F. 「NAS 存储 + 穿透」双关(一个名字同时指向"存着"和"能到")
**F-1 英文双关型**(同一个词里同时有"盘/存储"和"通路")
| 显示名 | 标识 | 中文名建议 | 说明 |
| --- | --- | --- | --- |
| Driveway | `driveway` | 盘路 | drive(盘/硬盘)+ way(通路)——字面是"车库前的车道",放在存储语境里刚好是"盘往外走的路",双关最巧;缺点是常见英文词,域名与商标被占概率高,必须先查 |
| Baylink | `baylink` | 盘链 | bay(NAS 的"盘位")+ link(连接);7 字符、自造度高,检索干净 |
| Bayway | `bayway` | 盘途 | bay + way,5 字符,短而奇;英文里有同名小地名,软件领域少见 |
| Portage | `portage` | 搬运 | 指在两条不通的水道之间把船与货搬过去,与"把 NAS 里的东西搬到公网"完全同构;**但 Gentoo 的包管理器就叫 Portage,技术圈认知已被占满,不建议** |
**F-2 存储器物型**("存"义明确,通路那层交给标语)
| 显示名 | 标识 | 中文名建议 | 说明 |
| --- | --- | --- | --- |
| Tansu | `tansu` | 箪笥 | 日语「箪笥」=分层抽屉的日式储物柜;冷僻、有质感,画面也很适合做图标 |
| Coffer | `coffer` | 藏柜 | 英语"保险箱",5 字符,画面是"锁着的箱子";冷僻、少撞 |
| Attic | `attic` | 阁楼 | "东西放阁楼,要用时上去取"——正是家庭 NAS 的真实使用方式,亲切易懂 |
| Larder | `larder` | 储藏室 | 家里存食物的小间,冷僻且带生活气 |
| Granary | `granary` | 粮仓 | 囤粮之所,画面感最强;7 字符偏长 |
| Hako | `hako` | 箱 | 日语「箱」,4 字符、极简,适合走冷淡克制的产品调性 |
**F-3 中文拼音双关型**(仓/库 + 门/路/口)
| 显示名 | 标识 | 中文名 | 说明 |
| --- | --- | --- | --- |
| Canglu | `canglu` | 仓路 | cāng lù 完全对应:「仓」是 NAS 里存的东西、「路」是穿透出去的通路——一个名字两层意思,5 字符,`canglu-agent`、`canglu.db` 都顺 |
| Cangmen | `cangmen` | 仓门 | 「把自家仓门打开」,直指"在外面访问家里的东西"这个诉求;6 字符 |
| Chuku | `chuku` | 出库 | 「出库」=把仓库里的东西送出去,动作感最强,中文用户零解释成本;4 字符 |
| Kumen | `kumen` | 库门 | 与 cangmen 同义,更短、更有古意 |
| Cangdao | `cangdao` | 仓道 | 「仓中之道」,把存储与通路并列写出来;6 字符 |
| Kufang | `kufang` | 库房 | 最直白好懂;正因为通用,商标注册难度最大 |
**F-4 直白组合型**(NAS + 词:`nasway` / `nasport` / `nasbay`)
含义最准确、最不用解释,但都含通用缩写 `NAS`,商标几乎不可能注册,看起来也像内部工具名——**建议只用作副标题或仓库简介(例如「nasway — 为 NAS 而生的穿透面板」),不要当品牌名**。
#### 这条线上的首选三个
1. **`driveway` / 盘路** —— 双关最巧:drive 是硬盘、way 是通路,英文用户会心一笑,中文名「盘路」也说得通;代价是常见词,必须先核域名与商标。
2. **`canglu` / 仓路** —— 中文双关,拼音与标识完全对应,`canglu-agent`、`canglu.db`、`CANGLU_DATA_DIR` 都好念,且不与前几组任何候选重叠。
3. **`tansu` / 箪笥** —— 想要"存储器物"的气质又要冷僻好检索时的选择;分层抽屉的柜子本身就是个不错的图标母题。
> ⚠️ 一个诚实的提醒:让**一个名字同时说清"NAS 里存着"和"外面能到"**,很容易变成拼装词(nasway、cangdao 这类就在这个边缘)。如果只能保住一层意思,建议保"通路",把"NAS/存储"交给标语去说——例如「让家里的 NAS 走出内网」,名字本身留出生长空间。
#### 建议优先考虑的组合(按这个项目的实际处境排序)
1. **`portway` / 通途** —— 语义、标识、双语读感三项都最均衡;技术与中文用户都不需要解释。
2. **`dukou` / 渡口** —— 中文语境里最容易被记住的一个,产品定位讲一遍就懂;国际化时用英文标识兜住。
3. **`zhandao` / 栈道** —— 冷门、好检索、意象有情绪("在无路处开出一条路"),适合做社区故事。
4. **`fonara`** —— 想保住 Fo 音、把这次改动讲成"品牌升级"而非"另起炉灶"时的首选。
5. **`airlock` / 气闸** —— 想要强识别度的视觉符号(圆环阀门 + 状态灯,天然适合做状态图标)时的首选。
#### 已知撞名、不建议使用的名字
| 名字 | 为什么别用 |
| --- | --- |
| Pangolin / 穿山甲 | 是**同类开源项目本身**(`fosrl/pangolin`,见 `docs/feature-benchmark-2026-09-24.md` 的同类项目盘点),会被直接误认成它 |
| Harbor | CNCF 的镜像仓库项目,技术圈认知已被占满 |
| Warp / Warpgate / Skyway / Overpass / Lumen | 分别撞 Cloudflare WARP 与 Warp.dev、知名开源堡垒机、NTT 的 P2P 服务、OSM 查询 API、电信运营商与多个产品线 |
| Nexo / Zeta / Orbi | 本文初稿曾把它们当作"简洁短词"举例,实际分别撞加密借贷平台、多家同名公司、Netgear 的 Mesh 产品线;写进 `web/src/constants/app.ts` 之前先排除 |
| Portalis / Wayfare | 游戏/出行类同名产品较多,检索会被淹没 |
> ⚠️ 现名 `Fonu` 与罗曼语系词 `fono`(电话)高度接近,换名正好可以顺手绕开这类"念起来像另一个词"的问题——建议把「读起来/拼起来是否与既有词重合」列入上面的硬性约束。
**改名后自查清单(供阶段 5 使用)**:
```
# 全仓(排除 node_modules / dist / .data)
rg -in "fonu" --glob '!web/node_modules/**' --glob '!**/dist/**' --glob '!.data/**'
# 分类核查
rg -n "FONU_" # 环境变量:是否只剩新前缀
rg -n "fonu_session|fonu-theme" # 契约/本地状态
rg -n "fonu\\.db|/fonu-agent|fonu-probe" # 存储与远端残留
rg -n "O96u/Fonu|/fonu:" # 外部身份与镜像
rg -n "github.com/fonu/fonu" # module path(含 Dockerfile ldflags)
```
**正式定名前的四处占用自查(逐个到站内查,别指望搜索引擎)**
| 查什么 | 怎么查 | 判定标准 |
| --- | --- | --- |
| GitHub 组织 / 仓库 | `https://github.com/search?q=<标识>&type=repositories` 与组织页直连 | 组织名被占 → 无法建同名 org;同名仓库若是活跃的同类工具 → 直接换 |
| Docker Hub | 站内搜 `<标识>`,并确认 `<你的用户名>/<标识>` 可创建 | 镜像名一旦发布,改名只能新建仓库(见 §六) |
| 域名 | `.com` / `.net` / `.io` / `.cn` / `.dev` 的 whois | 核心后缀被竞品或商标方持有 → 减分;仅冷门后缀被占可接受 |
| 商标 | 中国商标网 + 目标市场(EUIPO / USPTO),查第 9 类(软件)、第 42 类(技术服务) | 存在同类同名的在先注册 → 直接排除,不改 |
> 本次实测:泛化的网页检索对"这个名字有没有被占用"几乎无效(只会返回无关内容),所以这四处必须站内查。另外把派生形态也念一遍——`<标识>-agent`、`<标识>.db`、`--<标识>-brand`、`<IDENT>_DATA_DIR`——它们是每天都要看见的东西。
---
## 八、风险与回滚
| 风险 | 触发条件 | 缓解 |
| --- | --- | --- |
| 老用户"数据丢失" | 只改 `DBPath()` 不做接管 | 阶段 3 的 rename + 失败回退;发布说明写明「首次启动会接管旧库」 |
| 会话密钥静默回落默认值 | 只改 env 前缀 | `FONU_SESSION_SECRET` 双读 + 启动告警;保留一个版本的旧前缀 |
| VPS agent 失联/漂移误报 | 服务端与 agent 版本错配 | 新旧路径/变量/标记双支持;`AgentVersion` 递增;安装脚本接管旧 unit |
| Docker 起来是空库 | volume 名变更 | compose 显式 `name:` + 迁移命令 + 用旧 volume 起新容器做验收 |
| 版本号显示 `dev` | 漏改 Dockerfile 的 `-ldflags` module path | 构建后必查版本接口;CI 加一条断言 |
| 客户端全部掉登录态 | cookie 改名 | 双读或接受一次重登(发布说明写明) |
| 前端版本检查失效 | GitHub 仓库改名后旧名被占/新常量未同步 | 改名后回归一次 `version.ts` 路径 |
**回滚策略**:阶段 1~3 的回滚以「保留旧名兼容分支」为主(一个 `git revert` 即可,因为兼容层是新增而非替换);数据库重命名是不可逆动作,但方向单一(旧→新),回退版本时提供一条反向重命名说明即可。
---
## 九、结论
- **可以做全面品牌升级,落点是有限且已全部找清的**(见 §一,含 5 层证据)。
- **不要全局替换**:`internal/config/config.go:50`(`fonu.db`)、`config.go:24,29,31-38`(`FONU_*`)、`internal/frp/agent_installer.go:35,36`(VPS 上 `/etc/fonu-agent/agent.env`)这三处是"改了会伤用户"的位置,必须先设计兼容再动手。
- **建议按 §四 的五阶段推进**,阶段 1 就能拿到"看着焕然一新"的结果,阶段 3 才是真正需要谨慎的工程部分;阶段 5 用 §七 的清单收尾。
- 本方案只做分析,未改动任何代码。
---
> 说明:本文基于 2026-09-25 的仓库状态(VERSION 1.2.12)实测盘点;外部平台(GitHub 重定向、Docker Hub 不支持改名)的结论来自当日检索,实施前建议再核实一次平台当前行为。
+273
View File
@@ -0,0 +1,273 @@
# Cloudflare Tunnel(cloudflared)与 Fonu 的融合方案
> 日期:2026-09-25 对象:Fonu 1.0.2(VERSION 1.2.12)
> 方法:先查 Cloudflare 官方文档与同类开源项目,再逐个核对本仓库现有可复用面(下文带 `文件:行` 的都是读过代码得出的)。
> 结论速览:**值得做,但建议以「第二条出口」的形态并存,先做 locally-managed + 进程托管,不接 Cloudflare API**。详见 §五、§六。
---
## 一、为什么值得做(对 Fonu 的战略意义)
Fonu 现在的穿透链路**必须有公网服务端**:本机 `frpc` → VPS 上的 `frps` → 域名。用户得有一台带公网 IP 的云主机,还得自己放行端口、签证书、配 DNS。
Cloudflare Tunnel 把「服务端」换成 Cloudflare 的边缘网络:本机 `cloudflared` **主动向外**建立到边缘的长连接(出站 only),公网侧不需要任何监听端口,也不需要公网 IP——顺带白拿 CF 的 WAF / DDoS 防护与自动 HTTPS。
对 Fonu 的用户画像(家庭/个人 NAS)来说,这正好补上一类人:**没有实体公网 IP、或不想为穿透长期租 VPS**的人。
但要说清楚它**替代不了什么**(这决定了它是「并存」而不是「替换」):
| 场景 | frp | Cloudflare Tunnel |
| --- | --- | --- |
| 内网 HTTP(S) 网站/面板(域名访问) | ✅ | ✅ **更省事**(无需 VPS、无需放行端口) |
| 任意 TCP/UDP(游戏服、非 HTTP 协议) | ✅ | ⚠️ 需要访问端也装 `cloudflared`(`cloudflared access tcp`),不是通用 TCP 出口 |
| 客户端不需要额外软件的裸 TCP 端口 | ✅ | ❌ |
| 自持服务端 / 完全自主可控 | ✅ | ❌(流量必经 CF 边缘) |
| 大文件/视频流量 | ✅(自己带宽说话) | ⚠️ CF 条款限制非 Enterprise 的 CDN 视频/大文件用途,见 §七 |
所以定位是:**HTTP 类公网访问走 Cloudflare 更划算,TCP 类与自主性要求高的仍走 frp**。
---
## 二、同类项目(检索结果)
| 项目 | 做什么 | 对 Fonu 的借鉴 |
| --- | --- | --- |
| [WisdomSky/Cloudflared-web](https://github.com/WisdomSky/Cloudflared-web)(≈451★,Vue) | Docker 镜像里打包 `cloudflared` + 简单 Web UI,用来 start/stop **remotely-managed** 隧道 | **最接近的同位产品**:证明「面板管 cloudflared 进程」这个形态成立;但它的能力止步于开关,规则仍在 CF 面板里——这正是 Fonu 可以做得更好的地方(规则同源生成) |
| [adyanth/cloudflare-operator](https://github.com/adyanth/cloudflare-operator)(≈691★) | Kubernetes Operator,用 **CF API** 创建/管理 Tunnel 与 DNS | 演示了「API 驱动」的完整闭环;但需要 API Token + Account/Zone ID,是第二个凭据面(Fonu 第一轮不建议走,见 §六) |
| [qingchencloud/cftunnel](https://github.com/qingchencloud/cftunnel)(≈338★) | 轻量隧道控制 | 佐证需求存在 |
| [brenner-tobias/addon-cloudflared](https://github.com/brenner-tobias/addon-cloudflared) | Home Assistant 插件,locally-managed 与 remotely-managed 两种模式都支持 | HA 社区的做法可参考:**两种模式并存**,文档把差异讲透 |
| [cloudflare/cloudflared](https://github.com/cloudflare/cloudflared) | 官方客户端(Apache-2.0) | 我们的被托管对象;GitHub Release 提供多平台二进制 |
| 对照项:[Pangolin](https://github.com/fosrl/pangolin) / Tailscale Funnel / ngrok | 同类「免公网 IP」方案 | Tailscale Funnel 只开 HTTPS 且端口数量有限、ngrok 免费档收紧;CF Tunnel 免费档最宽松,这也是它值得优先支持的原因 |
`docs/feature-benchmark-2026-09-24.md` 里已经把 Pangolin 列为架构最近的同类项目;Cloudflare Tunnel 的差别在于:**它把「服务端」外包给了 Cloudflare,Fonu 只需管好本机客户端**。
---
## 三、cloudflared 的两种托管模型(选型的关键)
| | locally-managed(本地管理) | remotely-managed(远程管理) |
| --- | --- | --- |
| 凭据 | `cert.pem`(`cloudflared tunnel login` 浏览器授权)+ `<UUID>.json` credentials | 一个 **token**(`eyJ...`,CF 面板生成) |
| 规则来源 | 本机 `config.yml` 的 `ingress:` | **CF 面板 / API**(本地 `config.yml` 会被**忽略**) |
| 建隧道 | `cloudflared tunnel create <name>` | CF 面板,或 `POST /accounts/{id}/cfd_tunnel` |
| DNS | `cloudflared tunnel route dns <tunnel> <hostname>` | CF 面板自动建 CNAME |
| 与 Fonu 的价值取向 | ✅ **契合**:反代规则由 Fonu 同源生成(和现在生成 `frps.toml` / nginx vhost 一个路子) | ⚠️ 冲突:规则跑到 CF 面板去了,Fonu 沦为「进程开关」 |
官方文档确认了这个分界([配置参考](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/local-management/configuration-file)),社区也踩过坑:给 remotely-managed 隧道传 `--config` 时配置会被云端覆盖,日志里出现 `No ingress rules were defined ... cloudflared will return 503`([cloudflared#1029](https://github.com/cloudflare/cloudflared/issues/1029))。
**结论:第一轮只支持 locally-managed。** 若用户只想粘贴 token,提供「仅托管进程」的降级模式并**在界面上明确写清「规则由 Cloudflare 面板管理」**,不要让用户以为 Fonu 生成的规则生效了。
> 补充(参考 §九 后修正):这个二分法不够细。参考实现证明**token 可以解码**(base64 的 JSON,含 `AccountTag` / `TunnelID` / `TunnelSecret`),用它自己生成 `creds.json` 就能按 locally-managed 跑本地 `config.yml`;再配合「云端 ↔ 本地 ingress 互相同步」,两种模式实际上是可切换的。所以 Fonu 的正确做法不是「二选一」,而是**显式区分并同步**:当前生效的是本地 `config.yml` 还是云端 ingress,以及「推送/拉取」两个动作。
### 健康探测(这点比 frp 更省事)
`cloudflared tunnel --metrics 127.0.0.1:<port> run --config ...` 会暴露 HTTP 端点,其中 **`/ready` 在有到边缘的活跃连接时返回 200**(CF 官方在 k8s livenessProbe 里就是这么用的)。Fonu 现在为了判断「隧道通不通」要绕 frps dashboard,还专门加过 `tunnel_known` 来区分「判不出来」和「不通」;CF Tunnel 则有明确的就绪端点,可以直接喂给现有健康链路。
---
## 四、本仓库可复用的面(已核对代码)
| 能力 | Fonu 现有实现 | 复用方式 |
| --- | --- | --- |
| 二进制下载 / 启用 | `Service.ListReleases`(`internal/frp/service.go:112`)、`ListBinaries`(`:120`)、`ActivateBinary`(`:258`);`internal/frp/runtime.go` 的 `fetchOfficialReleases` / `listBinaries` / `readActiveBinary` / `writeActiveBinary` / `versionFromBinaryPath` | cloudflared 同样发 GitHub Release,**同一套框架泛化成「可指定仓库」即可**(现在 URL 拼的是 frp 的 release 页) |
| 客户端进程托管 | `internal/frp/manager.go`:`os.OpenFile(logPath, O_CREATE\|O_APPEND\|O_WRONLY, 0600)` + `cmd.Stdout/Stderr` 重定向 + 状态/退出原因 + `tailFile` 读日志尾 | 与 `cloudflared tunnel run` 完全同构,可直接复用(日志追加、不截断的语义也一致) |
| 凭据加密与回显 | `secret.Box` + `maskedToken = "********"`(`internal/frp/service.go:23`)+ 「预览脱敏 / 下发原文」两分支约定(v1.2.12 刚修) | token / credentials JSON 存加密字段;**预览一律掩码** |
| 配置生成 | `internal/frp/frps_template.go`、`internal/proxy` 的 nginx vhost 模板 | `config.yml` 的 `ingress:` 用同样思路生成,注释与安全提示的风格照搬 |
| 健康与可用率 | `internal/monitor`(`Source` 接口 + `HealthRecorder`)、`migrations/026` 的健康事件表、`metrics_samples` | 给 `Source` 加一个 provider(frp / cloudflared),`/ready` 作为判活依据 |
| 日志界面 / 公开状态页 | 日志页各页签、`/status` 公开状态页(默认关闭 + 访问码 + 限流) | 新隧道类型直接进现有页面,不用另造一套 |
| 规则模型 | `frp.Proxy{ServerID, Type, LocalIP, LocalPort, CustomDomains, Options}`(`internal/frp/model.go:186`) | 不再需要 `ServerID` 时用一个 provider 字段代替(见方案 B) |
---
## 五、三个方案
### 方案 A:独立模块「Cloudflare 隧道」(推荐第一轮)
新增一个与「公网服务端(FRP)」并列的模块,**不碰现有 frp 规则模型**:
- 管理一份 cloudflared 二进制(复用 §四 的下载/启用框架)。
- 管理 N 个隧道实例,每个实例 = 一份 `config.yml` + credentials + 日志文件 + 子进程。
- `ingress` 规则**由 Fonu 生成**:从本机已有的反代规则里挑「要暴露的域名」,映射到 `http://127.0.0.1:<本地端口>`,模板与 nginx vhost 同源风格;末尾固定给 catch-all `service: http_status:404`(CF 要求最后一条兜底)。
- 健康:`--metrics` + `/ready`,写入现有健康与可用率链路;通知复用(隧道断开/恢复)。
- 前端:与「公网服务端」同构的列表 + 详情(状态 / 日志 / 配置预览 / 下发)。
**优点**:改动封闭、不动现有模型、可独立上线、风险低。
**代价**:规则要在两处维护(frp 规则 vs CF 隧道规则)——这是方案 B 要解决的。
### 方案 B:把「隧道出口」抽象成 provider(v2 方向)
在规则上加 `provider: frp | cloudflare`(`frp.Proxy` 增字段,配套迁移),规则本身只说「哪个域名 → 内网哪个端口」,出口由 provider 决定:
- 好处:一条规则可在 FRP / CF 之间切换,健康、可用率、状态页、漂移检测天然统一。
- 成本:动模型 + 迁移 + 前端多处(规则表单、健康四段、公网反代页),且 `ServerID` 的语义要重新定义(CF 侧没有服务端)。
- 建议:**等方案 A 稳定、用户真的需要在两种出口间迁移规则时再做**。
### 方案 C:只做引导(不接管进程)
只生成 `config.yml` 与 DNS 指引,让用户自己跑 `cloudflared`。
**不建议**:不接管进程就没有状态、日志、健康、可用率,等于放弃 Fonu 的全部价值。
---
## 六、推荐落地路径(分阶段)
### 阶段 1:只读与引导(最低风险,先验证需求)
1. 新增「Cloudflare 隧道」入口:粘贴 **token**(remotely-managed)或上传 **credentials JSON + tunnel UUID**(locally-managed),凭据加密存储。
2. 生成 `config.yml` 预览(**脱敏**,沿用 v1.2.12 的预览/下发两分支约定)并把文件写到 `<DataDir>/cloudflared/<name>/config.yml`。
3. 界面给出 CF 侧操作指引(建隧道、CNAME、`cloudflared tunnel route dns`),**不接管进程**。
4. 明确提示模式差异:locally-managed 才由 Fonu 生成规则。
### 阶段 2:托管进程与健康(核心价值)
5. 把 §四 的 release/binaries 框架泛化,支持下载 `cloudflared` 并启用指定版本;进程托管复用 `Manager`(日志、状态、退出原因、`tailFile`)。
6. `--metrics 127.0.0.1:<port>` + `/ready` 接入 `internal/monitor`:新增 provider 判活,接入现有通知与 `metrics_samples` 可用率。
7. 子命令支持:启动 / 停止 / 重启 / 重载配置 / 查看日志(与「公网服务端」的操作面板一致)。
### 阶段 3:规则同源与界面完善
8. 从本机反代规则推导 `ingress`(勾选要暴露的域名),生成后一键下发并重启;末尾自动补 catch-all。
9. 前端补齐:状态卡、日志页签、配置预览(脱敏)、与 VPS 实际文件的漂移检测(沿用「配置漂移」的交互)。
10. Access(Zero Trust)相关只做**诊断提示**(例如「此域名未启用 Access 保护」),不代配置策略。
### 阶段 4(可选,需单独评估)
11. 接 Cloudflare API 自动建隧道与 DNS:需要 API Token(权限 `Account: Cloudflare Tunnel: Edit` + `Zone: DNS: Edit`)+ Account ID + Zone ID。**这是第二个凭据面**,收益是省掉几步手工操作,代价是多一份高权限凭据要保管——建议等前面稳定后再评估,并且只在用户显式开启时启用。
---
## 七、风险与边界(务必写进功能说明)
**条款与合规**
- Tunnel 本身免费、不限条数/带宽,但 Cloudflare 的 CDN 条款限制「用 CDN 提供视频与其他大文件」(非 Enterprise 需购买 Stream/Images 等付费服务)。社区里这条措辞有过调整,但仍应视为风险:**用 CF Tunnel 做影音/大文件分发不在我们的推荐用法内**,界面上要提示。
- TryCloudflare(快速隧道:`cloudflared tunnel --url http://localhost:PORT`)官方明确「不保证 SLA 与可用性,仅供测试与开发」,域名随机、每次重启变化——**不作为正式功能**,最多做成「临时演示」按钮。
**网络可用性**
- 中国大陆访问 CF 边缘的延迟与稳定性不稳定,这对 Fonu 的用户群是**现实问题**:建议在文档与界面上对比说明「CF 适合海外访问优先的场景,国内链路以 frp + 自持 VPS 更可控」。
- 出站 only 意味着 VPS 上不需要任何放行端口,但也就**读不到**「CF 侧有哪些域名指向了这条隧道」——相关校验只能靠 `cloudflared` 自身命令,不能靠 agent。
**实现边界**
- **不做 remotely-managed 的规则生成**(会被云端覆盖,见 §三)。
- **不代配 Access 策略**(只提示状态)。
- **不改现有 frp 链路**:CF Tunnel 是并存出口;`internal/frp` 的规则、健康、状态页语义保持不变。
- **不做通用 TCP 出口**:需要访问端装 `cloudflared` 的场景超出「NAS 面板」的定位,只做说明。
- 凭据全部走 `secret.Box` 加密 + 掩码回显;日志与 API 响应绝不出现明文 token(沿用 `maskedToken` 与「预览脱敏」约定)。
---
## 九、参考实现分析:`cf-tunnel-manager`(用户自有项目,Gitee `li-hua-88/cf-tunnel-manager`)
> 项目:**Tunnel-Cloudflared-Manager** v1.0.1,Rust + Actix-web + Tokio + Serde,前端原生 JS + Tailwind(`static/`),JSON 文件存储,MIT;Linux systemd(`tcm`/`cftm` 运维脚本)+ Docker 两套交付。
> 本节结论来自逐个读到源码的模块:`src/services/{tunnel,binary,cloudflare_api}.rs`、`src/models/tunnel.rs`、`src/utils/{config_helper,paths}.rs`、`Cargo.toml`。
### 9.1 它的定位与能力
| 维度 | 它做了什么 |
| --- | --- |
| 隧道类型 | **命名隧道**(token 或 config-file 启动)+ **快速隧道**(`--url`) |
| 进程管理 | `TunnelProcessManager`:`HashMap<名称, Child>` + 独立的 `login_process` + 快速隧道 URL 缓存 |
| 日志 | `tokio::process` 逐行读 stdout/stderr → **WebSocket 广播**(`tunnel-log` 事件),前端实时滚动 |
| 指标 | 每 2 秒抓 `cloudflared` 的 Prometheus 端点 → 同样用 WebSocket 广播(`tunnel-metrics`) |
| 二进制 | `src/services/binary.rs` 自管下载/替换/检测,不要求用户预装 |
| 账号模式 | `--origincert` + `tunnel create` + `tunnel route dns`,把生成的 `creds.json` 搬进自己的 `data/tunnels/<名>/` |
| 云端同步 | `src/services/cloudflare_api.rs` 能按 tunnel **拉取/推送** ingress 配置(`fetch_tunnel_config` / `push_tunnel_config`) |
| 登录与安全 | JWT + bcrypt 管理员账号,默认 `admin`/`admin666`(首次登录强制改密) |
| 网络调优 | `protocol`(auto/http2/quic,Windows 或检测到代理时回落 http2)、`ha-connections`、`edge-ip-version`、`originRequest`(`caPool`/`noTLSVerify`/`httpHostHeader`)、代理环境变量开关 |
### 9.2 值得直接借鉴的六个做法
**① token 可以解码成凭据,于是 token 模式也能本地管规则**
`initialize_token_tunnel` 把 CF 隧道 token 解成 `{AccountTag, TunnelID, TunnelSecret}` 并写出 `creds.json` + `config.yml`——即「用户只给一个 token,但规则仍由面板本地生成」。这直接提升了 Fonu 方案的上限(§三 的补充已按此修正)。
**② metrics 端口不去猜,从日志里读出来**
命名隧道启动时**故意不传 `--metrics`**,让 cloudflared 用默认端口段(20241-20245),然后从 stderr 解析 `INF starting metrics server on 127.0.0.1:65432/metrics` 拿到真实端口——天然避开端口冲突(Fonu 现在给 frps dashboard 固定端口,多实例时会撞)。
**③ `localhost` 必须归一化成 `127.0.0.1`**
`add_tunnel_endpoint` 里把 `localhost` 换成 `127.0.0.1`,注释写明「避免 Cloudflare 将其转换为本地网络 IP」。这是 CF 侧的真实坑,Fonu 生成 `ingress` 时要照做。
**④ 始终保留最后一条 catch-all**
新增端点插在「数组倒数第一位之前」(`insert_pos = len - 1`),末条永远是 `service: http_status:404`。Fonu 改 `ingress` 时必须沿用这个不变式,否则配置校验不过。
**⑤ 带宽指标:官方 metrics 没有 HTTP/2 字节数,它做了系统级兜底**
- QUIC 下直接读 `quic_client_receive_bytes` / `quic_client_sent_bytes` / `quic_client_smoothed_rtt`。
- HTTP/2 下官方无字节数,改用**按进程采 TCP 连接字节**:Linux 走 netlink `SOCK_DIAG_BY_FAMILY` + `/proc/<pid>/fd` 取 socket inode;Windows 走 `GetExtendedTcpTable` + `GetPerTcpConnectionEStats`(需要管理员,失败时给 `windows_estats_requires_admin` 降级原因)。
- 并且**只统计公网远端 IP**(过滤私有/回环/链路本地/`100.64/10` CGNAT),延迟兜底用**主动 HEAD 探测** ingress 里的公网 hostname(带 `?__cf_probe=<nonce>` + no-cache)。
> 对 Fonu 的意义:这一块可以**先不做**(我们已有规则级流量与可用率),但「降级要给出明确原因而不是空白」这个原则要学。
**⑥ Windows 下起子进程要隐藏控制台窗口**
`creation_flags(CREATE_NO_WINDOW)`(`0x08000000`)。Fonu 本机在 Windows 上跑,托管 `cloudflared.exe` 时必须带上,否则会闪黑窗。
### 9.3 与 Fonu 的差异(决定「借鉴什么、不借鉴什么」)
| 维度 | cf-tunnel-manager | Fonu 该怎么做 |
| --- | --- | --- |
| 职责 | **专做** Cloudflare Tunnel 的一个独立面板 | 把 CF 作为**第二条出口**并入现有规则/健康/通知/状态页体系 |
| 存储 | JSON 文件(`data/*.json`)+ `data/tunnels/<名>/` | 继续用 SQLite + 迁移;凭据进 `secret.Box` 加密字段 |
| 规则来源 | 面板自己的 ingress 列表(+ 云端同步) | 从**本机已有反代规则**推导(同源生成),避免第二份手工维护 |
| 健康 | metrics 端点 scraping + 主动探测(仅前端展示) | 接入 `internal/monitor` 的 `Source`/`HealthRecorder`,落库可用率 + 复用告警与通知 |
| 实时日志 | WebSocket 广播(专为「看着它滚」设计) | 沿用现有「读日志文件 + 日志页」模型(与 frpc 一致,无需引入 WS) |
| 多实例 | 多隧道并行(每隧道一个子进程) | 同模;但用 SQLite 建 `tunnels` 表而不是内存 HashMap |
| 认证 | 自带 JWT + 账号体系 | 复用 Fonu 现有会话 + API Token |
### 9.4 对方案本体的修订建议
1. **方案 A 的第一阶段就加「凭据导入」两条路**:`token`(解码出 creds.json)与 `cert.pem + credentials JSON`(账号模式),二者最终都落到「本地 `config.yml` 由 Fonu 生成」。
2. **阶段 2 把「metrics 端口从日志发现」写成实现约定**,不要固定端口。
3. **阶段 3 增加「云端 ↔ 本地同步」两个动作**(拉取 / 推送),并在界面上标明当前生效侧——这是参考项目比原方案更完整的地方。
4. **`ingress` 生成器锁定三条不变式**:末条 catch-all、`localhost` → `127.0.0.1`、插入位置在 catch-all 之前;并补一个「生成后自检」的纯函数测试(参照本仓 `maskFRPSSecrets` 的测试写法)。
---
## 十、IP 优选:能不能做、能做到哪一步
### 结论一句话
**隧道侧(本机 `cloudflared` → CF 边缘)可以优选,访问侧(浏览器 → CF 边缘)不行。** 而国内用户最在意的那一段延迟往往在访问侧——所以不要把它宣传成「提速神器」,它的收益是**可控但有限**的。
### 隧道侧:三条可用手段(已核对官方文档与变更日志)
| 手段 | 具体做法 | 适用与代价 |
| --- | --- | --- |
| **选协议** | `--protocol quic` / `http2` | 国内 QUIC(UDP 7844)常被 QoS 掉,`http2`(TCP 7844)反而稳;这是**成本最低、收益最直接**的一项 |
| **选地址族** | `--edge-ip-version 4` / `6` / `auto`,或环境变量 `TUNNEL_EDGE_IP_VERSION` | ⚠️ **2026.4.0 起默认值由 `4` 改成 `auto`**(官方 CHANGES 里的 Breaking Change)——`auto` 会跟着系统解析器走,国内环境若 IPv6 路由差就可能反而变慢。**建议 Fonu 显式下发 `--edge-ip-version 4`**(除非用户自己有 IPv6 好线路) |
| **优选边缘 IP** | 对 `region1.v2.argotunnel.com` / `region2.v2.argotunnel.com` 做延迟测速,选中最快的官方边缘 IP,写 hosts 覆盖解析 | 官方文档给出固定端点与端口:这类域名指向 CF 的 IPv4 段(`198.41.192.7`~`198.41.192.127`、`162.159.234.1`~`162.159.234.10`)与 **端口 7844(TCP/UDP)**。这是社区「Tunnel 优选 IP」的通行做法 |
**优选时两条不能错的约束**:
1. **两个 region 都要覆盖**。官方预检文档要求健康部署有**四个活跃 replica、同时连上 `region1` 与 `region2`**(或对应地区域名)——只改其中一个域名,另一半连接会跑到未优选(乃至不通)的路径上,净效果可能是变差。
2. **IP 列表不能硬编码**。CF 改过端点与 hostname(例:`proxy-dns` 整条功能已在 2026.2.0 被移除),优选表要么给默认值 + 可配置,要么每次从官方 DNS 拉候选集再测。
关于实例数:`--ha-connections` 决定每个 region 开几条连接,也是「多路择优」的天然手段(参考实现 §9.1 已把它做成设置项)。
### 访问侧:Tunnel 模式下做不到
用 CF Tunnel 时,公域域名必须 CNAME 到 `<tunnel-uuid>.cfargotunnel.com` 并且**开启代理(橙云)**——解析结果由 CF 决定,**用户无法自选边缘 IP**。社区那套「测速 + 自选 IP」之所以常见,是因为他们用的是「CNAME 到优选 IP + 关闭橙云 + 自己回源」的方案;那**恰好是 Tunnel 不能走的路**(关掉代理就没有 Tunnel 了)。
也就是说:换域名解析 / 让访客改 hosts 都不现实,**访问侧这一段在 Tunnel 架构下就是交给 CF 的**。
### 对 Fonu 方案的落点
| 阶段 | 做什么 | 依据 |
| --- | --- | --- |
| 阶段 2(建议直接做) | 「网络调优」三项做成设置项:`protocol`(默认 http2)、`edge-ip-version`(**默认 4**)、`ha-connections`;以命令行参数下发 | 参考实现已验证可行,且改一项就能治国内 QUIC 被打压的场景 |
| 阶段 3(可选) | 「边缘优选」工具:并发探测官方 IPv4 段的 **TCP 7844 握手延迟** → 生成「两个 region 各 1~2 个优选 IP」的 hosts 建议;**页面展示而不是代写 hosts**(Windows 改 hosts 需管理员,Fonu 不应越权),并提供「复制这段内容」+ 生效检测 | 沿用本仓「不越权、降级要说明原因」的口径 |
| 阶段 3(可选) | 优选前后对比:用**已有的** `metrics_samples` 延迟/可用率序列展示效果,而不是另造一套测速图 | 复用 §四 的健康与指标链路 |
### 必须写进功能说明的三句实话
1. 优选只影响「本机 → CF 边缘」这一段;访客到自己那侧 CF 边缘的那段,Fonu 与用户都控制不了。
2. 所以它**不是**「翻墙提速」类工具:国内访问 CF 的稳定性受链路与运营商影响,优选只能把「本机出口到边缘」这一段挑好。
3. 官方端点会变(见上),因此把所有 IP/域名做成**可配置 + 有默认值 + 可一键恢复默认**。
---
## 十一、给用户的一句话结论(可直接对外)
> Fonu 现在支持两类公网出口:**frp**(自持 VPS,任意 TCP/UDP,完全自主)与 **Cloudflare 隧道**(无需公网 IP 与 VPS,HTTP 类最省事,自带 CF 防护)。
> 要裸 TCP 端口、要完全自主,用 frp;要让内网 Web 服务零端口暴露地上公网,用 Cloudflare 隧道。
> Cloudflare 隧道还提供网络调优(协议 / 地址族 / 连接数)与**边缘 IP 优选**(仅作用于本机到 CF 边缘这一段,访客侧无法自选)。
---
> 说明:本文只做方案分析,未改动任何代码。实施时沿用本仓库既有约定:每步验证 `go build/vet/test` + `vue-tsc/vite build`、agent 行为变更递增 `AgentVersion`、提交同批更新 `CHANGELOG` 与 `VERSION`、密钥相关内容必须过一遍「预览脱敏 / 下发原文」两头检查。