From a14d92ccbb6c81663b9d2718ad72f208b67b6e19 Mon Sep 17 00:00:00 2001 From: mrlee <470349233@qq.com> Date: Fri, 2 Oct 2026 14:38:00 +0800 Subject: [PATCH] docs: add fnOS unified gateway plan --- docs/fnos-unified-gateway-plan.md | 890 ++++++++++++++++++++++++++++++ 1 file changed, 890 insertions(+) create mode 100644 docs/fnos-unified-gateway-plan.md diff --git a/docs/fnos-unified-gateway-plan.md b/docs/fnos-unified-gateway-plan.md new file mode 100644 index 0000000..b0838ac --- /dev/null +++ b/docs/fnos-unified-gateway-plan.md @@ -0,0 +1,890 @@ +# Havline 飞牛统一网关应用改造方案 + +> 日期:2026-10-02 +> 适用范围:`fnos/havline` 飞牛应用包与 Havline 主程序 1.9.x +> 官方依据:飞牛应用开放平台「统一网关」「应用入口」「应用资源」「环境变量」「Docker 应用案例」 + +## 一、结论先行 + +统一网关不是 Docker 应用模板的替代品,而是飞牛应用的一种**访问模型**。 + +官方文档明确说明:Docker 应用也可以使用统一网关。做法是把 +`${TRIM_APPDEST}` 挂载到容器内,让容器中的服务在安装目录下创建 +Unix Socket,再由 fnOS 网关转发请求。 + +因此 Havline 的正确改造方向是: + +```text +Docker 运行形态(保留) ++ 统一网关访问模型(新增) ++ 端口服务回退(保留) +``` + +而不是把现有 Docker 应用整体推翻重做。 + +推荐首版边界: + +```text +管理界面: + 通过 /app/havline 和 Unix Socket 暴露 + +公网反代、DDNS、证书、FRP、Cloudflare Tunnel: + 仍由 Havline 自己管理 + +端口 18080/9443: + 作为公网反代功能端口和回退入口保留 +``` + +## 二、统一网关是什么 + +飞牛统一网关为应用提供飞牛 fnOS 访问域名下的稳定系统 URL。 + +请求路径: + +```text +/app/{appname} +/app/{appname}/{customPath} +``` + +工作流程: + +1. 用户在飞牛桌面打开应用。 +2. fnOS 先校验用户会话。 +3. fnOS 将请求转发到应用声明的 Unix Socket。 +4. 应用读取网关注入的用户 Header。 +5. 应用继续执行自己的业务权限校验。 + +统一网关与端口服务的对比: + +| 能力 | 端口服务 | 统一网关 | +| --- | --- | --- | +| 访问地址 | 独立端口 | `/app/{appname}` | +| NAS 登录态 | 不接入 | 转发前校验 | +| 用户上下文 | 无 | `X-Trim-*` Header | +| WebSocket | 应用自行处理 | 支持 | +| 常驻服务 | 支持 | 支持 | +| 部署形态 | 任意 | 任意,Docker 也支持 | +| 核心适配 | 监听端口 | 监听 Unix Socket | + +## 三、官方字段与约束 + +### 3.1 应用入口 + +统一网关入口写在 `app/ui/config`: + +```json +{ + ".url": { + "havline.main": { + "title": "Havline", + "icon": "images/icon_{0}.png", + "type": "iframe", + "protocol": "", + "gatewayPrefix": "/app/havline", + "gatewaySocket": "app.sock", + "url": "/app/havline", + "allUsers": false, + "control": { + "accessPerm": "readonly" + } + } + } +} +``` + +字段规则: + +```text +gatewayPrefix + 必须使用 /app/{appname} 或 /app/{appname}/{customPath} + 发布后应保持稳定 + +gatewaySocket + 只填写 Socket 文件名,例如 app.sock + 实际路径为 ${TRIM_APPDEST}/app.sock + +protocol / port + 在统一网关入口中被忽略,不参与路由 +``` + +### 3.2 Docker 应用接入 + +官方 Docker 案例给出的结构: + +```yaml +services: + web: + image: my-app:latest + container_name: hello-docker-web + restart: unless-stopped + volumes: + - "${TRIM_APPDEST}:/app/target:rw" + environment: + GATEWAY_SOCKET: /app/target/app.sock +``` + +容器内服务监听: + +```text +/app/target/app.sock +``` + +宿主机对应路径: + +```text +/var/apps/{appname}/target/app.sock +``` + +### 3.3 用户 Header + +网关校验会话后注入: + +| Header | 含义 | 示例 | +| --- | --- | --- | +| `X-Trim-Userid` | 当前用户 UID | `1000` | +| `X-Trim-Isadmin` | 是否管理员 | `true` / `false` | +| `X-Trim-Username` | 当前用户名 | `admin` | + +这些 Header 只能作为网关可信上下文使用。应用仍然需要校验: + +```text +接口权限 +资源归属 +文件路径 +高风险操作 +``` + +不能信任客户端自己传入的用户 ID。 + +## 四、当前 Havline 的差距 + +### 4.1 当前监听方式 + +```text +HAVLINE_LISTEN=:6893 +``` + +主程序使用 TCP 端口监听: + +```text +internal/config/config.go + ListenAddr = HAVLINE_LISTEN,默认 :6893 + +internal/app/app.go + http.Server{Addr: cfg.ListenAddr} + server.ListenAndServe() +``` + +当前没有 Unix Socket 监听能力。 + +### 4.2 当前飞牛入口 + +```json +{ + ".url": { + "havline.main": { + "title": "Havline", + "icon": "images/icon_{0}.png", + "type": "iframe", + "protocol": "http", + "port": "${wizard_service_port}", + "url": "/", + "allUsers": false + } + } +} +``` + +这是端口服务入口,不是统一网关入口。 + +### 4.3 当前前端路径假设 + +```text +Vue Router: + createWebHistory(),无 base + +API 客户端: + 直接使用 /api/... 绝对路径 + +SSE / EventSource: + /api/proxies/{id}/logs/stream + +静态资源: + 由 Vite 默认根路径构建 +``` + +统一网关路径是 `/app/havline`,这些绝对路径不能直接复用。 + +### 4.4 当前容器权限 + +Dockerfile 没有显式 `USER`,容器默认以 root 运行。 + +这对创建 Socket 文件是有利的: + +```text +/app/target/app.sock +``` + +但如果后续改为非 root 用户运行,需要额外处理 Socket 的属主和权限。 + +### 4.5 当前端口映射 + +```yaml +ports: + - "${wizard_service_port}:6893" + - "${wizard_nginx_http_port}:80" + - "${wizard_nginx_https_port}:443" +``` + +统一网关只替代管理界面的入口,不会替代 Havline 的反向代理端口。 + +## 五、目标架构 + +### 5.1 访问分层 + +```text +飞牛桌面 + -> https://fnOS域名/app/havline + -> 飞牛统一网关校验登录态 + -> /var/apps/havline/target/app.sock + -> Havline 网关监听器 + -> Havline API / Web UI + +Havline 公网反代功能 + -> 18080/9443 端口 + -> Havline 内置 Nginx + -> 用户配置的 NAS 服务 +``` + +### 5.2 包结构目标 + +```text +fnos/havline/ +├── manifest +├── app/ +│ ├── docker/ +│ │ └── docker-compose.yaml +│ └── ui/ +│ └── config +├── config/ +│ ├── privilege +│ └── resource +├── cmd/ +│ ├── main +│ ├── install_callback +│ ├── upgrade_callback +│ ├── config_callback +│ └── uninstall_callback +└── wizard/ + ├── install + ├── config + └── uninstall +``` + +### 5.3 Docker Compose 目标 + +```yaml +services: + havline: + image: jayhub.top/mrlee/havline:${TRIM_APPVER}-amd64-docker + container_name: havline + restart: unless-stopped + ports: + - "${wizard_nginx_http_port}:80" + - "${wizard_nginx_https_port}:443" + volumes: + - "${TRIM_PKGVAR}:/data" + - "${TRIM_APPDEST}:/app/target:rw" + environment: + HAVLINE_DATA_DIR: /data + HAVLINE_LISTEN: ":6893" + HAVLINE_GATEWAY_SOCKET: "/app/target/app.sock" + HAVLINE_GATEWAY_PREFIX: "/app/havline" + HAVLINE_TRUST_GATEWAY_HEADERS: "false" + HAVLINE_NGINX_HTTP_PORT: "80" + HAVLINE_NGINX_HTTPS_PORT: "443" + HAVLINE_UPDATER_SOCKET: "" + HAVLINE_INITIAL_ADMIN_PASSWORD: "${wizard_initial_admin_password}" + TZ: "${wizard_timezone}" +``` + +说明: + +```text +HAVLINE_LISTEN 保留,用于端口回退和内部健康检查 +HAVLINE_GATEWAY_SOCKET 指向统一网关 Socket +HAVLINE_GATEWAY_PREFIX 用于前端 base path 和路由 +HAVLINE_TRUST_GATEWAY_HEADERS 第一阶段关闭 +wizard_service_port 可以从端口映射中移除 +``` + +### 5.4 manifest 目标 + +首版建议: + +```ini +appname = havline +version = <版本> +display_name = Havline +desc = NAS 公网访问管理:反向代理、DDNS、HTTPS 证书、FRP、Cloudflare Tunnel 与一键升级 +source = thirdparty +platform = all +maintainer = Havline +maintainer_url = https://jayhub.top/mrlee/Havline +distributor = Havline +desktop_uidir = ui +desktop_applaunchname = havline.main +checkport = false +ctl_stop = true +service_port = 6893 +``` + +`service_port` 是否保留需要实测: + +```text +如果只注册统一网关入口: + 可以不依赖 service_port + +如果同时保留端口回退入口: + 保留 service_port 便于调试 +``` + +## 六、三种实现路线 + +### 方案 A:Go 主程序原生监听 Unix Socket + +实现: + +```text +internal/config + 增加 GatewaySocket、GatewayPrefix 配置 + +internal/app + 同时创建 TCP listener 和 Unix listener + TCP 用于端口回退 + Unix 用于统一网关 +``` + +伪代码: + +```go +tcpServer := &http.Server{Addr: cfg.ListenAddr, Handler: handler} +unixServer := &http.Server{Handler: handler} + +if cfg.GatewaySocket != "" { + _ = os.Remove(cfg.GatewaySocket) + listener, err := net.Listen("unix", cfg.GatewaySocket) + if err != nil { + return err + } + if err := os.Chmod(cfg.GatewaySocket, 0o660); err != nil { + return err + } + go unixServer.Serve(listener) +} + +go tcpServer.ListenAndServe() +``` + +优点: + +```text +无额外进程 +可以精确处理 base path +可以按来源区分 TCP 与网关请求 +可以安全启用 X-Trim-* SSO +网络性能更好 +``` + +缺点: + +```text +需要修改 Go 主程序 +需要补充 Unix Socket 生命周期测试 +``` + +推荐度:高。 + +### 方案 B:容器内 Nginx 监听 Unix Socket + +实现: + +```text +容器内 Nginx 监听 /app/target/app.sock +反向代理到 127.0.0.1:6893 +``` + +优点: + +```text +不改 Go 主程序 +可以快速做统一网关 PoC +``` + +缺点: + +```text +多一层代理 +base path 和 Header 处理复杂 +错误排查链路更长 +SSO 头部的可信边界不清晰 +``` + +推荐度:中,适合过渡验证,不建议作为长期方案。 + +### 方案 C:单独 Sidecar 容器 + +实现: + +```text +新增 sidecar 容器监听 Unix Socket +代理到 havline 容器 +``` + +优点: + +```text +主容器不增加进程 +``` + +缺点: + +```text +Compose 复杂度高 +生命周期和升级耦合 +收益不明显 +``` + +推荐度:低。 + +## 七、前端 base path 改造 + +统一网关路径固定为: + +```text +/app/havline +``` + +前端需要支持可配置 base path。 + +### 7.1 Vite + +目标: + +```ts +export default defineConfig({ + base: process.env.VITE_BASE_PATH || '/', + // ... +}) +``` + +构建飞牛统一网关版本时: + +```text +VITE_BASE_PATH=/app/havline/ +``` + +### 7.2 Vue Router + +当前: + +```ts +createWebHistory() +``` + +目标: + +```ts +createWebHistory(import.meta.env.BASE_URL) +``` + +或者由运行时 `HAVLINE_GATEWAY_PREFIX` 注入。 + +### 7.3 API 客户端 + +当前: + +```ts +fetch('/api/...') +``` + +目标: + +```ts +const API_BASE = import.meta.env.BASE_URL.replace(/\/$/, '') +fetch(`${API_BASE}/api/...`) +``` + +或者由前端运行时配置读取: + +```text +window.__HAVLINE_BASE_PATH__ +``` + +### 7.4 SSE / EventSource + +当前: + +```ts +new EventSource('/api/proxies/{id}/logs/stream') +``` + +目标: + +```ts +new EventSource(`${API_BASE}/api/proxies/{id}/logs/stream`) +``` + +### 7.5 WebSocket + +目标路径: + +```text +/app/havline/ws +``` + +前端必须使用当前页面主机和协议: + +```ts +const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:' +const wsUrl = `${wsProtocol}//${location.host}/app/havline/ws` +``` + +### 7.6 Cookie + +需要确认 Havline 的会话 Cookie 是否设置了 Path。 + +建议: + +```text +端口模式:Path=/ +网关模式:Path=/app/havline +``` + +或者统一使用 `Path=/`,减少改造量。 + +### 7.7 CSRF / Origin + +如果 Havline 有 Origin 或 CSRF 校验,需要把以下来源加入白名单: + +```text +https:// +https:///app/havline +``` + +## 八、认证与权限设计 + +### 8.1 第一阶段:双登录,低风险 + +```text +飞牛网关校验 fnOS 登录态 +Havline 仍然显示自己的登录页 +用户登录 Havline 后正常使用 +``` + +优点: + +```text +改造最小 +不影响端口模式 +不引入 SSO 安全问题 +``` + +缺点: + +```text +用户需要登录两次 +``` + +建议首版采用。 + +### 8.2 第二阶段:网关 SSO + +启用条件: + +```text +HAVLINE_TRUST_GATEWAY_HEADERS=true +只有 Unix Socket 请求才信任 X-Trim-* Header +``` + +映射规则建议: + +```text +X-Trim-Isadmin=true + -> 映射到 Havline admin + +X-Trim-Isadmin=false + -> 默认拒绝管理界面 + -> 或后续映射为只读用户 + +缺少 X-Trim-Userid + -> 视为非网关请求,回退到本地登录 +``` + +安全要求: + +```text +Unix Socket 文件权限限制为网关和 Havline 进程可访问 +TCP 端口请求绝对不能信任 X-Trim-* Header +网关 Header 只用于身份上下文,不替代 Havline 业务权限 +``` + +## 九、Havline 功能边界 + +统一网关只解决飞牛系统域名下的应用入口,不替代 Havline 的核心功能。 + +| Havline 功能 | 统一网关是否替代 | 说明 | +| --- | --- | --- | +| 管理界面入口 | 是 | 改为 `/app/havline` | +| fnOS 登录态 | 部分 | 网关校验登录,Havline 仍需业务鉴权 | +| 内置 Nginx 反向代理 | 否 | 仍由 Havline 管理 | +| DDNS | 否 | 仍由 Havline 管理 | +| HTTPS 证书申请 | 否 | 仍由 Havline 管理 | +| FRP 内网穿透 | 否 | 仍由 Havline 管理 | +| Cloudflare Tunnel | 否 | 仍由 Havline 管理 | +| 公网域名反代 | 否 | 仍由 Havline 管理 | +| 端口 18080/9443 | 否 | 作为 Havline 功能端口保留 | + +不要把 Havline 的公网反代、证书和隧道功能迁入飞牛统一网关,否则会丢失 +Havline 作为独立公网访问管理工具的能力。 + +## 十、生命周期与打包变更 + +### 10.1 安装回调 + +需要确保: + +```text +${TRIM_APPDEST} 存在 +${TRIM_APPDEST} 权限允许容器写入 app.sock +${TRIM_PKGVAR} 数据目录权限正确 +``` + +### 10.2 启动 + +容器启动时: + +```text +删除 ${TRIM_APPDEST}/app.sock 残留文件 +创建新的 Unix Socket +设置 Socket 权限 +``` + +### 10.3 升级 + +升级时: + +```text +gatewayPrefix 保持不变 +gatewaySocket 文件名保持不变 +数据目录保持不变 +Socket 文件重新创建 +``` + +### 10.4 卸载 + +卸载时: + +```text +删除 ${TRIM_APPDEST}/app.sock +默认保留 ${TRIM_PKGVAR} +``` + +## 十一、分阶段实施计划 + +### 阶段 0:官方行为 PoC + +目标:用最小 Docker 示例验证统一网关。 + +必须验证: + +```text +[ ] ${TRIM_APPDEST} 挂载路径 +[ ] Socket 文件权限 +[ ] /app/havline 请求是否保留前缀 +[ ] Header 是否原样注入 +[ ] WebSocket 是否可用 +[ ] 应用重启后 Socket 是否自动重建 +``` + +产出: + +```text +docs/fnos-unified-gateway-poc.md +``` + +### 阶段 1:Havline 支持 Unix Socket + +目标:主程序可以同时监听 TCP 和 Unix Socket。 + +改造: + +```text +internal/config +internal/app +启动日志 +停止清理 +``` + +验收: + +```text +[ ] TCP 6893 正常 +[ ] Unix Socket 正常 +[ ] Socket 权限正确 +[ ] 容器重启后 Socket 重建 +``` + +### 阶段 2:前端 base path + +目标:Havline 可以在 `/app/havline` 下完整运行。 + +改造: + +```text +Vite base +Vue Router base +API client base +EventSource base +WebSocket base +Cookie Path +CSRF / Origin +``` + +验收: + +```text +[ ] 页面打开 +[ ] 静态资源加载 +[ ] 登录 +[ ] API +[ ] SSE +[ ] WebSocket +[ ] 刷新深层路由 +``` + +### 阶段 3:飞牛包切换统一网关入口 + +目标:桌面入口走 `/app/havline`。 + +改造: + +```text +app/ui/config +docker-compose.yaml +``` + +保留: + +```text +18080/9443 端口 +端口回退入口 +``` + +验收: + +```text +[ ] 飞牛桌面入口打开正确 +[ ] 不需要端口入口即可访问管理界面 +[ ] 18080/9443 功能不受影响 +``` + +### 阶段 4:网关 SSO + +目标:使用 `X-Trim-*` 自动识别飞牛管理员。 + +改造: + +```text +HAVLINE_TRUST_GATEWAY_HEADERS=true +只对 Unix Socket 请求生效 +管理员映射 +非管理员处理策略 +``` + +验收: + +```text +[ ] 管理员免登录 +[ ] 非管理员被正确拒绝或只读 +[ ] TCP 请求伪造 Header 无效 +``` + +### 阶段 5:体验收敛 + +目标:统一网关成为默认入口,端口仅保留给 Havline 自身功能。 + +验收: + +```text +[ ] 飞牛桌面入口只显示统一网关入口 +[ ] 端口入口不再出现在应用卡片 +[ ] 升级、卸载、重装正常 +``` + +## 十二、验收清单 + +```text +[ ] gatewayPrefix 稳定为 /app/havline +[ ] gatewaySocket 稳定为 app.sock +[ ] ${TRIM_APPDEST} 正确挂载 +[ ] Havline 监听 Unix Socket +[ ] Socket 启动前清理残留文件 +[ ] Socket 权限满足网关访问 +[ ] 前端使用 /app/havline base +[ ] API、SSE、WebSocket 路径正确 +[ ] Cookie 和 CSRF 正常 +[ ] 端口回退仍可用 +[ ] 飞牛管理员可通过网关访问 +[ ] 非管理员访问策略明确 +[ ] 升级后数据不丢 +[ ] 卸载默认保留数据 +[ ] amd64 与 arm64 镜像可启动 +``` + +## 十三、风险与未决问题 + +| 风险 | 影响 | 处理 | +| --- | --- | --- | +| 网关是否剥离 `/app/havline` 前缀未知 | 前端 base path 可能不一致 | 阶段 0 PoC 实测 | +| Socket 属主和权限未知 | 网关无法访问 Socket | PoC 验证并在启动时 chmod | +| 前端绝对路径较多 | 网关下页面或 API 404 | 统一引入 base path | +| SSE / WebSocket 路径 | 实时日志和状态页不可用 | 阶段 2 专项测试 | +| 双登录体验 | 用户需要登录两次 | 阶段 4 再做 SSO | +| 伪造 `X-Trim-*` Header | 越权风险 | 只信任 Unix Socket 请求 | +| 端口与网关并存 | 入口混乱 | 桌面只注册网关入口 | +| Havline 公网反代功能边界 | 用户误解为被替代 | 文档明确说明 | +| 飞牛审核要求 | 上架被拒 | 提交前复核官方审核项 | +| 镜像多架构兼容 | 飞牛 Docker 28.5.2 对 OCI index 兼容性有限 | 使用 Docker schema2 兼容格式 | + +## 十四、推荐落地顺序 + +```text +1. 做最小统一网关 Docker PoC +2. 确认前缀、Socket 权限、Header、WebSocket +3. 主程序增加 Unix Socket 监听 +4. 前端增加可配置 base path +5. 飞牛包注册统一网关入口 +6. 保留端口回退 +7. 实测通过后再做网关 SSO +8. 最后收敛桌面入口 +``` + +## 十五、结论 + +Havline 不需要从 Docker 应用模板重写成另一种应用,而应在现有 Docker +应用上叠加统一网关访问模型。 + +推荐的最终形态: + +```text +运行形态:飞牛 Docker 应用 +访问模型:统一网关 /app/havline +服务监听:TCP 6893 + Unix Socket app.sock +用户体系:首版保留 Havline 登录,后续再做网关 SSO +功能边界:Havline 继续管理公网反代、DDNS、证书、FRP 和 Cloudflare Tunnel +回退能力:保留 18080/9443 端口 +``` + +这样既能获得飞牛系统统一的 HTTPS 域名、登录态和桌面入口体验,又不会 +破坏 Havline 作为独立 NAS 公网访问管理工具的核心能力。