Files
havline-native/app-src/docs/fnos-unified-gateway-plan.md
T

18 KiB
Raw Blame History

Havline 飞牛统一网关应用改造方案

日期:2026-10-02 适用范围:fnos/havline 飞牛应用包与 Havline 主程序 1.9.x 官方依据:飞牛应用开放平台「统一网关」「应用入口」「应用资源」「环境变量」「Docker 应用案例」

一、结论先行

统一网关不是 Docker 应用模板的替代品,而是飞牛应用的一种访问模型。

官方文档明确说明:Docker 应用也可以使用统一网关。做法是把 ${TRIM_APPDEST} 挂载到容器内,让容器中的服务在安装目录下创建 Unix Socket,再由 fnOS 网关转发请求。

因此 Havline 的正确改造方向是:

Docker 运行形态(保留)
+ 统一网关访问模型(新增)
+ 端口服务回退(保留)

而不是把现有 Docker 应用整体推翻重做。

推荐首版边界:

管理界面:
  通过 /app/havline 和 Unix Socket 暴露

公网反代、DDNS、证书、FRP、Cloudflare Tunnel:
  仍由 Havline 自己管理

端口 18080/9443:
  作为公网反代功能端口和回退入口保留

二、统一网关是什么

飞牛统一网关为应用提供飞牛 fnOS 访问域名下的稳定系统 URL。

请求路径:

/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:

{
  ".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"
      }
    }
  }
}

字段规则:

gatewayPrefix
  必须使用 /app/{appname} 或 /app/{appname}/{customPath}
  发布后应保持稳定

gatewaySocket
  只填写 Socket 文件名,例如 app.sock
  实际路径为 ${TRIM_APPDEST}/app.sock

protocol / port
  在统一网关入口中被忽略,不参与路由

3.2 Docker 应用接入

官方 Docker 案例给出的结构:

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

容器内服务监听:

/app/target/app.sock

宿主机对应路径:

/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 只能作为网关可信上下文使用。应用仍然需要校验:

接口权限
资源归属
文件路径
高风险操作

不能信任客户端自己传入的用户 ID。

四、当前 Havline 的差距

4.1 当前监听方式

HAVLINE_LISTEN=:6893

主程序使用 TCP 端口监听:

internal/config/config.go
  ListenAddr = HAVLINE_LISTEN,默认 :6893

internal/app/app.go
  http.Server{Addr: cfg.ListenAddr}
  server.ListenAndServe()

当前没有 Unix Socket 监听能力。

4.2 当前飞牛入口

{
  ".url": {
    "havline.main": {
      "title": "Havline",
      "icon": "images/icon_{0}.png",
      "type": "iframe",
      "protocol": "http",
      "port": "${wizard_service_port}",
      "url": "/",
      "allUsers": false
    }
  }
}

这是端口服务入口,不是统一网关入口。

4.3 当前前端路径假设

Vue Router:
  createWebHistory(),无 base

API 客户端:
  直接使用 /api/... 绝对路径

SSE / EventSource:
  /api/proxies/{id}/logs/stream

静态资源:
  由 Vite 默认根路径构建

统一网关路径是 /app/havline,这些绝对路径不能直接复用。

4.4 当前容器权限

Dockerfile 没有显式 USER,容器默认以 root 运行。

这对创建 Socket 文件是有利的:

/app/target/app.sock

但如果后续改为非 root 用户运行,需要额外处理 Socket 的属主和权限。

4.5 当前端口映射

ports:
  - "${wizard_service_port}:6893"
  - "${wizard_nginx_http_port}:80"
  - "${wizard_nginx_https_port}:443"

统一网关只替代管理界面的入口,不会替代 Havline 的反向代理端口。

五、目标架构

5.1 访问分层

飞牛桌面
  -> https://fnOS域名/app/havline
  -> 飞牛统一网关校验登录态
  -> /var/apps/havline/target/app.sock
  -> Havline 网关监听器
  -> Havline API / Web UI

Havline 公网反代功能
  -> 18080/9443 端口
  -> Havline 内置 Nginx
  -> 用户配置的 NAS 服务

5.2 包结构目标

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 目标

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}"

说明:

HAVLINE_LISTEN 保留,用于端口回退和内部健康检查
HAVLINE_GATEWAY_SOCKET 指向统一网关 Socket
HAVLINE_GATEWAY_PREFIX 用于前端 base path 和路由
HAVLINE_TRUST_GATEWAY_HEADERS 第一阶段关闭
wizard_service_port 可以从端口映射中移除

5.4 manifest 目标

首版建议:

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 是否保留需要实测:

如果只注册统一网关入口:
  可以不依赖 service_port

如果同时保留端口回退入口:
  保留 service_port 便于调试

六、三种实现路线

方案 A:Go 主程序原生监听 Unix Socket

实现:

internal/config
  增加 GatewaySocket、GatewayPrefix 配置

internal/app
  同时创建 TCP listener 和 Unix listener
  TCP 用于端口回退
  Unix 用于统一网关

伪代码:

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()

优点:

无额外进程
可以精确处理 base path
可以按来源区分 TCP 与网关请求
可以安全启用 X-Trim-* SSO
网络性能更好

缺点:

需要修改 Go 主程序
需要补充 Unix Socket 生命周期测试

推荐度:高。

方案 B:容器内 Nginx 监听 Unix Socket

实现:

容器内 Nginx 监听 /app/target/app.sock
反向代理到 127.0.0.1:6893

优点:

不改 Go 主程序
可以快速做统一网关 PoC

缺点:

多一层代理
base path 和 Header 处理复杂
错误排查链路更长
SSO 头部的可信边界不清晰

推荐度:中,适合过渡验证,不建议作为长期方案。

方案 C:单独 Sidecar 容器

实现:

新增 sidecar 容器监听 Unix Socket
代理到 havline 容器

优点:

主容器不增加进程

缺点:

Compose 复杂度高
生命周期和升级耦合
收益不明显

推荐度:低。

七、前端 base path 改造

统一网关路径固定为:

/app/havline

前端需要支持可配置 base path。

7.1 Vite

目标:

export default defineConfig({
  base: process.env.VITE_BASE_PATH || '/',
  // ...
})

构建飞牛统一网关版本时:

VITE_BASE_PATH=/app/havline/

7.2 Vue Router

当前:

createWebHistory()

目标:

createWebHistory(import.meta.env.BASE_URL)

或者由运行时 HAVLINE_GATEWAY_PREFIX 注入。

7.3 API 客户端

当前:

fetch('/api/...')

目标:

const API_BASE = import.meta.env.BASE_URL.replace(/\/$/, '')
fetch(`${API_BASE}/api/...`)

或者由前端运行时配置读取:

window.__HAVLINE_BASE_PATH__

7.4 SSE / EventSource

当前:

new EventSource('/api/proxies/{id}/logs/stream')

目标:

new EventSource(`${API_BASE}/api/proxies/{id}/logs/stream`)

7.5 WebSocket

目标路径:

/app/havline/ws

前端必须使用当前页面主机和协议:

const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
const wsUrl = `${wsProtocol}//${location.host}/app/havline/ws`

需要确认 Havline 的会话 Cookie 是否设置了 Path。

建议:

端口模式:Path=/
网关模式:Path=/app/havline

或者统一使用 Path=/,减少改造量。

7.7 CSRF / Origin

如果 Havline 有 Origin 或 CSRF 校验,需要把以下来源加入白名单:

https://<fnOS域名>
https://<fnOS域名>/app/havline

八、认证与权限设计

8.1 第一阶段:双登录,低风险

飞牛网关校验 fnOS 登录态
Havline 仍然显示自己的登录页
用户登录 Havline 后正常使用

优点:

改造最小
不影响端口模式
不引入 SSO 安全问题

缺点:

用户需要登录两次

建议首版采用。

8.2 第二阶段:网关 SSO

启用条件:

HAVLINE_TRUST_GATEWAY_HEADERS=true
只有 Unix Socket 请求才信任 X-Trim-* Header

映射规则建议:

X-Trim-Isadmin=true
  -> 映射到 Havline admin

X-Trim-Isadmin=false
  -> 默认拒绝管理界面
  -> 或后续映射为只读用户

缺少 X-Trim-Userid
  -> 视为非网关请求,回退到本地登录

安全要求:

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 安装回调

需要确保:

${TRIM_APPDEST} 存在
${TRIM_APPDEST} 权限允许容器写入 app.sock
${TRIM_PKGVAR} 数据目录权限正确

10.2 启动

容器启动时:

删除 ${TRIM_APPDEST}/app.sock 残留文件
创建新的 Unix Socket
设置 Socket 权限

10.3 升级

升级时:

gatewayPrefix 保持不变
gatewaySocket 文件名保持不变
数据目录保持不变
Socket 文件重新创建

10.4 卸载

卸载时:

删除 ${TRIM_APPDEST}/app.sock
默认保留 ${TRIM_PKGVAR}

十一、分阶段实施计划

阶段 0:官方行为 PoC

目标:用最小 Docker 示例验证统一网关。

必须验证:

[ ] ${TRIM_APPDEST} 挂载路径
[ ] Socket 文件权限
[ ] /app/havline 请求是否保留前缀
[ ] Header 是否原样注入
[ ] WebSocket 是否可用
[ ] 应用重启后 Socket 是否自动重建

产出:

docs/fnos-unified-gateway-poc.md

阶段 1:Havline 支持 Unix Socket

目标:主程序可以同时监听 TCP 和 Unix Socket。

改造:

internal/config
internal/app
启动日志
停止清理

验收:

[ ] TCP 6893 正常
[ ] Unix Socket 正常
[ ] Socket 权限正确
[ ] 容器重启后 Socket 重建

阶段 2:前端 base path

目标:Havline 可以在 /app/havline 下完整运行。

改造:

Vite base
Vue Router base
API client base
EventSource base
WebSocket base
Cookie Path
CSRF / Origin

验收:

[ ] 页面打开
[ ] 静态资源加载
[ ] 登录
[ ] API
[ ] SSE
[ ] WebSocket
[ ] 刷新深层路由

阶段 3:飞牛包切换统一网关入口

目标:桌面入口走 /app/havline。

改造:

app/ui/config
docker-compose.yaml

保留:

18080/9443 端口
端口回退入口

验收:

[ ] 飞牛桌面入口打开正确
[ ] 不需要端口入口即可访问管理界面
[ ] 18080/9443 功能不受影响

阶段 4:网关 SSO

目标:使用 X-Trim-* 自动识别飞牛管理员。

改造:

HAVLINE_TRUST_GATEWAY_HEADERS=true
只对 Unix Socket 请求生效
管理员映射
非管理员处理策略

验收:

[ ] 管理员免登录
[ ] 非管理员被正确拒绝或只读
[ ] TCP 请求伪造 Header 无效

阶段 5:体验收敛

目标:统一网关成为默认入口,端口仅保留给 Havline 自身功能。

验收:

[ ] 飞牛桌面入口只显示统一网关入口
[ ] 端口入口不再出现在应用卡片
[ ] 升级、卸载、重装正常

十二、验收清单

[ ] 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 兼容格式

十四、推荐落地顺序

1. 做最小统一网关 Docker PoC
2. 确认前缀、Socket 权限、Header、WebSocket
3. 主程序增加 Unix Socket 监听
4. 前端增加可配置 base path
5. 飞牛包注册统一网关入口
6. 保留端口回退
7. 实测通过后再做网关 SSO
8. 最后收敛桌面入口

十五、结论

Havline 不需要从 Docker 应用模板重写成另一种应用,而应在现有 Docker 应用上叠加统一网关访问模型。

推荐的最终形态:

运行形态:飞牛 Docker 应用
访问模型:统一网关 /app/havline
服务监听:TCP 6893 + Unix Socket app.sock
用户体系:首版保留 Havline 登录,后续再做网关 SSO
功能边界:Havline 继续管理公网反代、DDNS、证书、FRP 和 Cloudflare Tunnel
回退能力:保留 18080/9443 端口

这样既能获得飞牛系统统一的 HTTPS 域名、登录态和桌面入口体验,又不会 破坏 Havline 作为独立 NAS 公网访问管理工具的核心能力。