来源:intranet-tunnel/docs/AGENTS-ARCHIVE.md## NAS(飞牛 OS)客户端 Docker 部署(2026-09-13)(原文 2629 字符)

NAS(飞牛 OS)客户端 Docker 部署(2026-09-13)

目标:把内网穿透客户端用 Docker 部署到内网 NAS(<内网IP>,飞牛 OS / FNOS), 连上云服务器并端到端测试。结果:一次成功,三条隧道全部打通。

落点与配置

  • NAS 路径沿用其既有约定 /vol1/1000/docker/<项目名>(uid 1000 即当前用户);

这与云服务器的 /home/docker/<项目名>两套约定,不要混用。

  • 交付模板 deploy/docker/client/docker-compose.yml 只加了一行 volumes

(挂 ./tunnels.json),其余照用。

  • 客户端凭据不走面板 API(云上 AUTH_CAPTCHA_ENABLED=true,图形验证码挡住自动化),

用本文档前面记过的直接写库法:容器内取 TOKEN_SALTopenssl dgst -sha256 -hmac 算摘要,INSERT ... ON CONFLICT (client_id) DO UPDATEToken 明文只落在 NAS 的 client.env(权限 600),服务器上的临时脚本执行后立即删除。

三条可复用的判据

  1. TUNNELSTUNNELS_FILE 能力不同(文档一度把两者写成同一件事):

TUNNELS 只认 name:type:local_port[:domain] 简写 —— 写 JSON 会报 config: TUNNELS 项 ... 的本地端口非法;而且它的 local_addr硬编码为 127.0.0.1client/internal/config/config.goParseInlineTunnelsLocalAddr: "127.0.0.1")。 容器里的 127.0.0.1 是容器自己 ⇒ 容器场景只能用 TUNNELS_FILE。 已回写 client/.env.dockerdeploy/docker/client/client.env.example 与该目录 README (原先两处的 JSON 示例照抄必然启动失败)。

  1. 域名按 custom_domain 全库查占用,跨客户端会被拒绝ensureStaticTunnels

GetTunnelByDomain 命中且 owner.ClientID != clientID跳过该静态隧道 (日志 域名已被其它客户端占用,忽略静态隧道)。实测 NAS 上 fn(5666)、 qb(8085)、portainer 已被离线客户端 pc1 占着,所以 NAS 客户端只能用新前缀 (本次用 fnosmoviepilot)。

  1. 落在泛域内的新隧道,云端零改动:域名按前缀声明、服务端用 TUNNEL_DOMAIN 补全,

泛域块复用 ⇒ 边缘配置指纹不变、reload 次数不增,内置反代规则也随登录自动装载 (本次没有触发 POST /api/proxies/reload,也没有重启 tunnel-server)。 改完 tunnels.json 只需 docker compose restart tunnel-client; 而改 client.env 仍必须 up -d --force-recreateenv_file 只在创建容器时读)。

验证方法与工具坑

  • 端到端的强判据:不只看状态码,而是把经隧道的响应NAS 本机直连后端的响应

做 SHA256 逐字节比对(/dashboard/manifest.json 两条路径全等), 再叠加 X-Proxy-By: intranet-tunnel。这样能排除"公网某处恰好有同名页面"。

  • 流量记账查的是 stats(`tunnel_id, client_id, bytes_in, bytes_out, connections,

recorded_at),tunnels没有任何 bytes 列。⚠️ 该表存的是窗口增量而非累计值 (实测同一隧道 out` 539533 → 28250 → 2945 递减),解释"面板实时流量"时按这个口径。

  • ⚠️ Workbench CLI 的 exec 会破坏含双引号的命令(受控实验:

echo "A B" > /tmp/t2.txt; cat /tmp/t2.txt 只回显 A,换单引号正常)。 对策:命令一律用单引号,SQL 内层用 $$...$$(双引号被吞后 $$ 会在远程 shell 中 展开成 PID,查询直接失败);psql\d 也会因引号被剥离退化成 d 而报语法错。

  • ssh_upload 的本地路径围栏只放行用户 home(本环境会话工作区为空),

给 NAS 传文件前要先暂存到 %USERPROFILE% 下的临时目录,传完即删。

遗留

  • NAS 上本来就有飞牛系统自带的 frpc/var/apps/frpc,非容器),与本客户端并存、

互不抢端口(本客户端只出站)。日后排查"某域名走哪条通道"时要记得有两套。

  • 客户端镜像 1.0.4 与服务端 1.1.1 差 5 个版本(客户端源码自 v1.0.4 起未变更,

所以只是 tag 号旧)。另因服务端自签证书,客户端须 TLS_INSECURE=true

  • 库中残留离线客户端 pc1mobile-alert-test,其中 pc1 占着三个指向 NAS 的域名。

来源:intranet-tunnel/deploy/docker/client/README.md## 九、实测记录:飞牛 OS(FNOS)NAS(原文 3098 字符)

九、实测记录:飞牛 OS(FNOS)NAS

2026-09-13 在一台飞牛 OS(FNOS,Linux 6.18)NAS 上按本说明部署并跑通。 机型无关,前提是 Linux + Docker Engine 28.5 / Compose v5.1,且当前用户在 docker 组内。

落点与文件:

/vol1/1000/docker/intranet-tunnel-client/     # 沿用 NAS 既有的 /volN/<uid>/docker/<项目> 约定
├── client.env            # 权限 600;SERVER_ADDR / CLIENT_ID / TOKEN / TLS_INSECURE
├── docker-compose.yml    # 本目录的模板(volumes 里多解开一行 tunnels.json 挂载)
├── tunnels.json          # 静态隧道,local_addr 写 host.docker.internal
└── intranet-tunnel-client-images-1.0.4.tar

上面是当时(1.0.4)的落点记录,如实保留。自 1.2.0 起部署目录还应有 init.sh,并会生成 data/(配置库),见第三节与第五节。

实测结果:

  • 容器 Up (healthy)RestartCount=0;日志 已连接服务端并登录成功static_tunnels=N
  • 服务端 /healthzonline 由 0 变 1。
  • 三条隧道端到端可用(响应头均带 X-Proxy-By: intranet-tunnel):

uptime-kuma、飞牛 fnOS 面板(5666)、MoviePilot(40900)。

  • /healthztunnels 是「本地静态 + 服务端下发」的合计:本次 3 条本地
    • 1 条服务端下发 = 4,比 tunnels.json 里的条数多(见第七节)。
  • 新增隧道不需要改云端任何配置:域名都落在 *.tunnel.sushike.cloud 泛域内,

边缘 nginx 按泛域复用,配置与隧道条数解耦。改完 tunnels.json 只需 docker compose restart tunnel-client,不必 down

两个容易吃亏的地方:

  1. 域名不能跨客户端复用。服务端按 custom_domain 查占用,同域名已属于

另一个客户端时,静态隧道会被忽略(日志: 域名已被其它客户端占用,忽略静态隧道)。要接管旧前缀,先在服务端删掉原记录。

  1. client.env 必须 up -d --force-recreateenv_file 只在创建容器时读取),

而改 tunnels.jsonrestart 就够——两者要求不同,别互相套用。

自 1.2.0 起,隧道的权威来源是部署目录 data/ 下的配置库tunnels.json 只在首次建库时播种,之后在 Web 界面上增删改即可, 改完点「重启连接」或等下一次自动重连生效(比改文件 + restart 更直接)。

⚠️ 升级 / 重启前先 export DOCKER_CONFIG=/tmp/dsh-docker-config

根因不在 compose,而在家目录的属主。 实测那台 NAS 上 /home/ZYJ 的属主是 root:root(权限 755),普通用户建不了 ~/.docker,于是 compose 报

mkdir /home/ZYJ/.docker: permission denied

挂起约 60 秒才返回。⚠️ 只读子命令(ps / config / logs)完全不受影响, 所以很容易据此判定「compose 没问题」,把排查方向带偏。

# 临时绕过:把 Docker 的配置目录指到一个一定可写的位置
export DOCKER_CONFIG=/tmp/dsh-docker-config

# 根治(需 root):把家目录还给该用户。
# 用户名换成你部署时用的那个,组名以你的系统为准(这台 NAS 上是 Users)。
sudo chown <用户名>:Users /home/<用户名>

export 只对当前 shell 有效:换个终端(或每次新开会话)都要重新设一遍。 想一劳永逸,就走上面那条 chown 根治。

⚠️ stale 容器名:up 一直报 Conflict,可那个容器「看不见」

compose up 被中断之后(工具超时、但远程进程没被杀掉也会造成同样的结果), dockerd 的名字注册表里可能留下 <旧容器ID前12位>_<服务名> 这种占用,症状很反直觉:

  • docker inspect / docker rm 都报 no such container
  • docker ps -a 里也列不出它;
  • up 一直报

Conflict. The container name "/<旧ID>_tunnel-client" is already in use by container "..."

处置顺序是先删再 up

pgrep -af 'docker compose'      # 先看有没有残留的 compose 进程
kill -9 <PID>                   # 有就杀掉

docker rm -f tunnel-client      # 再删掉现有容器
docker compose up -d

为什么要「先删」:容器还在时,compose 会先把新容器建成 <旧ID>_<名字> 再改名, 正好撞上那个 stale 名;先把容器删掉,compose 就不必做这次重命名,从而绕开它。

⚠️ 隧道域名有两支泛域,实测只有一支生效

*.t.sushike.cloud*.tunnel.sushike.cloud两支不同的泛域, DNS、证书、边缘 nginx 三层各自独立 —— 配了一支不等于另一支可用。 本次 NAS 验证中的实测结果:

  • <服务名>.t.sushike.cloud TLS 握手失败SEC_E_WRONG_PRINCIPAL),

且这个名字解析到两个 IP

  • 全部验证最终走 *.tunnel.sushike.cloud 成功。

隧道域名的完整形式要与服务端的 TUNNEL_DOMAIN 对应:客户端侧只写前缀时, 由服务端按 TUNNEL_DOMAIN 补全 —— 所以换域名改的是服务端那一处, 不是逐条隧道去改。

技术文档库 / NAS 端部署 0 0 sushike
2026-09-13T12:41:54.351537575Z 2026-09-13T13:37:17.848508753Z