feat: create standalone native fnOS repository

This commit is contained in:
2026-10-02 15:19:09 +08:00
parent 69f3c30fab
commit 6725fc131d
492 changed files with 94023 additions and 3 deletions
+890
View File
@@ -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://<fnOS域名>
https://<fnOS域名>/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 公网访问管理工具的核心能力。