StunDeck 实战:飞牛 fnOS FPK、Docker 与 Cloudflare 302
在飞牛 fnOS 手动安装 StunDeck FPK,或使用 Docker 部署;再配置 Cloudflare 最小权限 API Token、代理 DNS 与 Single Redirect,让固定域名跟随动态 STUN 公网地址。
视频讲解 / 16:9
StunDeck 完整教程:Docker、Cloudflare 302/307 与 STUN 诊断
6 分钟完整操作演示,从部署、首次初始化和最小权限 Token,一路讲到映射服务、UPnP/NAT-PMP、六层诊断与独立外网验证。
点击播放即可观看完整讲解;支持进度拖动、画中画与全屏播放。
这篇教程的重点不是单独把 STUN 跑通,而是让用户始终访问一个固定域名:StunDeck 发现公网 IP 或动态端口变化后,自动更新 Cloudflare Single Redirect;Cloudflare 对入口域名返回 302(或 307),客户端再访问新的公网映射。
部署仍从 Docker 开始,但配置顺序必须是:部署 StunDeck → 创建 Cloudflare Token → 保存 Cloudflare 连接 → 创建 Redirect 服务 → 启动 STUN/UPnP → 验证 302 与第二跳。Cloudflare 不能等服务建完才补,否则创建服务时没有可选连接,也无法验证自动同步。
最终实测结果:
- Debian 13.6,amd64;
- Docker Engine 29.7.1,Docker Compose 5.3.1;
- StunDeck 镜像对应公开仓库提交
bcb5a1a; - Docker 服务环境没有代理,StunDeck 容器内的代理变量全部为空;
- NATMap 成功取得映射,UPnP 已把第一跳监听端口放行;
- 独立外网主机直连动态公网地址返回 HTTP 200;固定入口复查返回 HTTP 302 和
Location; - 没有把真实用户名、密码、Token、内网地址、公网 IP 或动态端口写入本文与配图。
STUN 不是中继,也不能把对称型 NAT 或受限 CGNAT 变成公网入口。本文记录的是在当前测试网络中验证成功的部署流程,不代表所有宽带环境都能复现相同结果。
飞牛 fnOS:直接安装正式 FPK
上架状态(2026-08-26):StunDeck 已提交飞牛应用中心上架审核。审核通过并公开上架前,可以先手动安装这里提供的正式 FPK。这里的飞牛安装包格式是 FPK,不是 Android APK。
当前正式版本为 StunDeck v0.1.1,支持 linux/amd64 与 linux/arm64。博客保留一份经过校验的正式发布包,同时提供 GitHub Release 作为备用来源:
- 从本站下载 stundeck-fpk-0.1.1.fpk(136,791 字节);
- 查看 StunDeck v0.1.1 GitHub Release;
- SHA-256:
63d7f0e00b3777400507e62033fb0cce72884d10ad824f51b435a502d3a5dd8f。
安装步骤:
- 下载 FPK,并在本机核对 SHA-256;
- 在飞牛 fnOS 打开「应用中心 → 设置 → 手动安装应用」;
- 选择
stundeck-fpk-0.1.1.fpk,按向导设置时区;只有通过 HTTPS 反向代理访问时才启用安全 Cookie; - 首次安装会从
20000–59999选择一个空闲端口并持久化保存,升级和重启不会重新随机; - 从飞牛桌面打开 StunDeck,创建管理员,再添加 Cloudflare Token、映射服务与 Webhook。
FPK 固定拉取不可变镜像 ghcr.io/nciae-zyh/stundeck:v0.1.1,因此 NAS 首次安装时需要能够访问 GHCR。安装包和向导不会预置或收集 Cloudflare Token、Webhook Secret 等秘密。
fnOS 会管理应用配置与持久化目录,真实数据位于包的 ${TRIM_PKGVAR}/data。升级、迁移或重装前,必须把 stundeck.db 与 master.key 成对备份;缺少 master.key 时,数据库中加密保存的 Cloudflare Token、Webhook Secret 与 TOTP Secret 无法恢复。卸载向导默认保留数据,只有明确选择“永久删除数据”时才会删除它。
先理解重点链路
用户始终访问固定域名
└─ Cloudflare 代理 DNS(橙色云)
└─ Single Redirect 返回 302 / 307 + Location
└─ 当前公网 IP:动态端口
└─ 主路由 UPnP / NAT-PMP / 手动端口映射
└─ StunDeck + NATMap
└─ 局域网目标服务
固定的是入口域名,不是公网端口。Cloudflare 只处理第一跳:它返回重定向响应,不会为第二跳提供 Tunnel、中继、WAF 或 Access。浏览器看到 Location 后会直接连接公网 IP 与动态端口。
这里有两个独立成功条件:
- 固定域名返回正确的 302/307,证明 DNS 与 Single Redirect 已同步;
- 跟随
Location后业务返回 200,证明 STUN、网关放行、防火墙与目标服务均可用。
只看到第一个结果,不能把服务标记为“公网可用”。
1. 部署前检查
准备一台 Linux 主机,建议满足:
- Debian 13/12 或 Docker 官方支持的其他发行版;
- amd64 或 arm64;
- 固定或可识别的局域网地址;
- 能访问局域网目标服务;
- 主路由允许按需启用 UPnP,或者你能配置手动端口转发;
- 准备一个无敏感数据、可以随时关闭的测试服务。
Lucky 的 STUN 文档建议在 NAT1 环境使用,并说明局域网主机需要 UPnP、端口转发或 DMZ 等额外条件。不同设备对 NAT 类型的命名并不统一,所以最终应以 STUN 日志与真正的外网回连为准。
不要把 NAS 管理后台、路由器后台或 StunDeck 控制面当作第一条公网测试规则。先使用静态测试页或临时 HTTP 服务。
2. 安装 Docker Engine 与 Compose
下面按 Docker 官方 Debian 安装文档配置 apt 仓库。Docker Compose 使用官方 docker-compose-plugin,不要再安装旧版独立 docker-compose。
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<'EOF'
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: trixie
Components: stable
Architectures: amd64
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
sudo apt install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin
如果不是 Debian 13 amd64,把 Suites 和 Architectures 改成实际值。可以用下面两条命令确认:
. /etc/os-release && echo "$VERSION_CODENAME"
dpkg --print-architecture
下载需要代理时,只让安装阶段使用
测试中,Docker 官方 GPG 地址有一次直连被重置。临时代理只用于下载,不应写进 /etc/environment,也不应长期留在 Docker systemd 服务中。
一个进程级示例:
PROXY_URL=http://192.168.50.9:7890
sudo curl --proxy "$PROXY_URL" -fsSL \
https://download.docker.com/linux/debian/gpg \
-o /etc/apt/keyrings/docker.asc
sudo env http_proxy="$PROXY_URL" https_proxy="$PROXY_URL" \
apt update
unset PROXY_URL
如果仓库持续不稳定,可以按 Docker 官方文档的“Install from a package”下载与系统匹配的 .deb,先校验文件,再在主机上安装。不要从网盘或不明镜像站获取 Docker 安装包。
安装后验证:
sudo docker version
sudo docker compose version
sudo systemctl is-enabled docker
sudo systemctl is-active docker
sudo systemctl show docker -p Environment
最后一条在本次环境中输出:
Environment=
这能证明 Docker daemon 没有继承常驻代理。它还不能证明容器没有代理,后面会再检查一次。
根据本次真实命令输出整理;主机名、登录身份、局域网地址和临时下载代理均已移除。
3. 编写 Compose 配置
创建目录:
sudo install -d -m 0755 /opt/stundeck
cd /opt/stundeck
保存为 /opt/stundeck/compose.yaml:
services:
stundeck:
image: ghcr.io/nciae-zyh/stundeck:latest
network_mode: host
restart: unless-stopped
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp:size=16m,mode=1777
volumes:
- stundeck-data:/var/lib/stundeck
environment:
STUNDECK_LISTEN: 0.0.0.0:8080
STUNDECK_DATA_DIR: /var/lib/stundeck
STUNDECK_SECURE_COOKIES: "false"
TZ: Asia/Shanghai
HTTP_PROXY: ""
HTTPS_PROXY: ""
ALL_PROXY: ""
http_proxy: ""
https_proxy: ""
all_proxy: ""
volumes:
stundeck-data:
几个关键点:
network_mode: host:NATMap 和 UPnP 需要看到 Linux 主机的真实网络栈;read_only、cap_drop: ALL、no-new-privileges:减少容器权限;- 只持久化
/var/lib/stundeck; - 显式清空大小写代理变量;
- 局域网 HTTP 首次部署使用
STUNDECK_SECURE_COOKIES: "false";如果以后由 HTTPS 反向代理访问,应改为"true"。
不要使用 Docker Desktop 作为正式 STUN 网关,也不要为了省事加入 privileged: true。
4. 拉取并启动
cd /opt/stundeck
sudo docker compose config --quiet
sudo docker compose pull
sudo docker compose up -d --no-build
sudo docker compose ps
本次环境的健康状态在约 10 秒后变为 healthy。继续验证:
curl -fsS http://127.0.0.1:8080/api/v1/health
sudo docker inspect stundeck-stundeck-1 \
--format '{{range .Config.Env}}{{println .}}{{end}}' \
| grep -i proxy
健康接口应返回 status: ok。代理检查会看到 6 个变量,但等号右侧都必须为空。镜像拉取时即使临时使用过代理,也要在启动 StunDeck 前撤销 daemon 代理并重启 Docker。
本次容器进入 healthy,健康接口返回 ok,容器内的大小写代理变量全部为空。画面未保留主机地址或容器标识。
5. 首次初始化与安全边界
从局域网打开:
http://服务器局域网地址:8080
首次进入需要自行创建管理员:
- 使用唯一用户名和至少 12 位的强密码;
- 访问模式选择“局域网”;
- 如果已经确定反向代理域名,可以填入允许访问的 Host;
- 登录后进入“安全设置”,再配置允许 Host 与验证器 2FA;
- 不要直接把 8080 控制面映射到公网。
一次性空数据环境中的真实首次初始化页面;用户名、密码与确认密码都没有填写。
域名 stun-lab-20260802.sparkles-editor.com 经本人确认可以公开,仅用于演示 Host 白名单;截图环境未保存该值。画面没有 DNS 记录、Zone/Rule ID、Token、TOTP 密钥或管理员身份。
如果必须远程管理控制面,优先使用 Cloudflare Tunnel、VPN 或其他带身份认证的反向代理。STUN 动态映射更适合发布可控业务端口,不适合暴露管理后台。
6. 先在 Cloudflare 创建最小权限 API Token
Single Redirect 只会处理经过 Cloudflare 代理的请求,因此入口域名必须存在于目标 Zone,并使用代理 DNS(橙色云)。本文不展示 DNS 记录值、Zone ID、Ruleset ID 或其他域名。
进入 Cloudflare 右上角用户菜单,打开“个人资料 → API 令牌”,点击“创建令牌”。
选择“创建自定义令牌”,不要使用 Global API Key。
建议使用可识别的名称,例如 StunDeck Redirect · sparkles-editor.com,然后添加三条 Zone 权限:
| 资源 | 权限 | 级别 | 用途 |
|---|---|---|---|
| 区域 | 区域 | 读取 | 查找并校验目标 Zone |
| 区域 | DNS | 编辑 | 自动创建或维护代理 DNS 记录 |
| 区域 | 单一重定向 | 编辑 | 创建和更新 302/307 Single Redirect |
在“区域资源”中选择“包括 → 特定区域 → sparkles-editor.com”。不要授权所有 Zone。若你关闭 StunDeck 的“自动管理 DNS”,可以不授予 DNS 编辑,但必须自行保证入口记录存在且处于代理状态。
客户端 IP 筛选只适合管理端出口固定的环境。家庭宽带出口会变化时,不要误把当前公网 IP 写死,否则 StunDeck 之后可能无法同步。可以按运维周期设置 Token 到期日。
点击“继续以显示摘要”,确认目标 Zone 只有三项权限:
最后点击“创建令牌”。Token 明文只显示一次:立即放入密码管理器并粘贴到 StunDeck,不要截图、不要写入 Compose、Markdown、Git、日志或聊天记录。本文为了避免创建不必要的长期凭据,截图停在复核页,没有展示或保存任何 Token 值。
7. 在 StunDeck 保存并检测 Cloudflare 连接
先进入 StunDeck 的“Cloudflare”页面,再创建映射服务:
- 连接名称填写易识别的名称;
- 粘贴刚创建的 API Token;
- 点击“检测权限”;
- 只在 Zone、DNS 和 Single Redirect 检测都通过后保存。
来自重建后的真实管理页面。输入框为空,截图不含 Token、账户名、DNS 值或任何资源 ID。
凭据会由 StunDeck 使用本地主密钥加密保存。备份时必须同时保留 SQLite 数据库与 master.key;缺少后者,连接凭据无法恢复。
8. 创建以 302 为核心的 Redirect 服务
现在进入“映射服务”,使用一项可以随时关闭的 HTTP 测试服务:
服务名称:家庭 NAS(示例)
局域网目标:192.168.1.20(示例)
目标端口:80(示例)
协议:TCP
监听端口:0(自动分配)
路由器端口放行:UPnP(推荐)
发布方式:Cloudflare Redirect
Cloudflare 连接:选择上一步保存的连接
入口域名:stun-lab-20260802.sparkles-editor.com
目标协议:HTTP
跳转状态:302 Temporary
自动管理 DNS:开启
保留路径:开启
保留查询参数:开启
真实页面中的文档表单;局域网地址和端口均为占位示例,Cloudflare 连接未保存,未提交这条服务。画面上方的实际服务内容已裁掉。
302 还是 307
| 状态 | 客户端行为 | 推荐场景 |
|---|---|---|
| 302 Temporary | 临时跳转;部分客户端会把 POST 改成 GET | 普通网页、下载页、以 GET 为主的服务,本文默认 |
| 307 Temporary Redirect | 临时跳转并保留 HTTP 方法与请求体 | API、Webhook、上传或不能改变 POST 语义的服务 |
两者都不会被当作永久跳转缓存。不要使用 301/308 表示会持续变化的 STUN 端点。
“保留路径”和“保留查询参数”开启后,访问 /files?a=1 会把路径与查询串拼到当前动态目标。使用 HTTPS 目标时还必须填写“HTTPS 目标域名”,并保证目标证书覆盖该域名;直接跳到公网 IP 通常无法通过证书主机名校验。
自动 DNS 只维护 StunDeck 自己创建并带有管理标记的记录。若同名记录由其他系统管理,StunDeck 会停止同步而不是强行覆盖;应先处理冲突,不能删除整组 DNS 后重建。
9. 启动服务并完成 STUN / UPnP 检测
点击“创建服务”,再点击“启动”。正常状态会依次经过:
已停止 → 探测中 → 路由已放行 → Cloudflare 已同步
StunDeck 的 UPnP 逻辑会把穿透监听端口映射到同一个本地端口,而不是把最终动态公网端口误当成本地端口。随后它把 NATMap 发现的公网 IP 与端口写入 Single Redirect 的目标。
点击服务卡片上的“STUN 检测”,逐项检查:
- 代理环境:Docker daemon 与容器都必须没有常驻代理;
- NATMap 程序:二进制可执行;
- 目标端口:StunDeck 主机能连接局域网服务;
- UDP/TCP STUN:至少与你所选协议对应的 Binding 成功;
- 路由器能力:发现正确的 UPnP IGD 或 NAT-PMP 网关;
- 路由器端口映射:映射已下发;
- 公网映射:已取得地址且更新时间合理;
- Cloudflare 同步:DNS 与 Redirect Rule 均已完成或明确报告冲突。
真实诊断结果的安全裁切;公网 IP、动态端口、内网地址与网关地址均不在画面中。
真实环境概览;没有展示服务地址、映射地址、凭据或 Cloudflare 配置。
10. 验证固定域名的 302 与第二跳
先禁止 curl 自动跟随跳转,只检查 Cloudflare 第一跳:
curl -sS -o /dev/null -D - --max-redirs 0 \
https://stun-lab-20260802.sparkles-editor.com/
应看到 HTTP/2 302(选择 307 时则为 307)以及 Location。Cloudflare 规则列表也应显示同一入口域名、302 状态与活动状态:
真实 Cloudflare 规则行;仅保留经允许公开的入口域名和 302 状态,动态公网目标、账户信息、其他规则与资源 ID 均未进入画面。
再从手机蜂窝网络或独立外网主机验证第二跳:
curl -sS -L --max-time 15 -o /dev/null \
-w 'status=%{http_code} redirects=%{num_redirects}\n' \
https://stun-lab-20260802.sparkles-editor.com/
目标是 status=200 redirects=1。本次重建中,独立外网主机直连新的动态映射得到 HTTP 200;固定入口也实测返回 302 和 Location。不过截图所示的旧隔离服务随后已下线,所以它现在只能证明第一跳规则仍生效,不能作为第二跳在线承诺。教程因此明确要求每次映射变化后同时检查这两层。
不要把真实 Location、公网 IP 与端口贴在文章、截图或工单里。它会变化,也会增加不必要的扫描风险。
11. 常见故障定位
已取得公网映射,但浏览器是 ERR_EMPTY_RESPONSE
按顺序检查:
- 局域网目标是否从 StunDeck 主机可达;
- 目标服务是否监听正确地址,而不是只监听
127.0.0.1; - 容器是否使用 host network;
- StunDeck 服务是否选择了正确的网关模式;
- UPnP/NAT-PMP 是否在真实出网网关下发映射;
- 多层 NAT 时,上一级光猫或路由器是否仍阻断入站;
- 是否用真正的外网测试,而不是在不支持 NAT Loopback 的局域网里访问公网地址。
STUN 时好时坏
- 确认 Docker daemon 与容器都没有代理;
- 关闭会改变出口的透明代理、策略路由或多 WAN 规则后对比;
- 保持监听端口唯一;
- 不要把动态端口当成固定端口写入客户端;
- 观察映射更新时间与 NATMap 进程是否持续运行;
- 对称型 NAT、运营商 CGNAT 或入站过滤严格时,改用 Tunnel、VPN 或中继方案。
固定域名没有返回 302/307
按顺序检查:
- 入口 DNS 是否为代理状态(橙色云);
- Token 是否限定到正确 Zone;
- 是否具备 Zone 读取与单一重定向编辑权限;
- 开启自动 DNS 时,是否额外具备 DNS 编辑权限;
- 同名 DNS 或 Redirect Rule 是否由其他系统管理而发生冲突;
- Cloudflare Rules 中是否存在
http_request_dynamic_redirect阶段的活动规则。
302 能打开入口,但第二跳失败
302 只更新入口。直接请求动态公网地址;如果仍失败,问题在 STUN、路由器映射、防火墙或目标服务,而不是 Cloudflare Redirect Rule。
12. 更新、备份与卸载
更新前把 SQLite 数据库和 master.key 一起备份,二者缺一不可。Compose 命名卷通常为 stundeck_stundeck-data,先确认实际名称:
sudo docker volume ls
sudo docker volume inspect stundeck_stundeck-data
更新:
cd /opt/stundeck
sudo docker compose pull
sudo docker compose up -d --no-build
sudo docker compose ps
停止但保留数据:
sudo docker compose down
只有确定不要恢复时才删除卷:
sudo docker compose down -v
down -v 会删除管理员、服务、事件记录、Cloudflare 加密凭据以及本地主密钥,无法通过重新拉取镜像恢复。
参考资料
- StunDeck 开源仓库
- StunDeck v0.1.1 Release 与 FPK
- 飞牛 fnOS 开发者文档:fnpack
- Docker:Install Docker Engine on Debian
- Docker:Install the Compose plugin
- Lucky:STUN 内网穿透
- NATMap 开源仓库
- Cloudflare:创建 API Token
- Cloudflare:通过 Dashboard 创建 Single Redirect
- Cloudflare:Single Redirect 设置与代理 DNS 要求
这次重建最重要的结论是把验证拆成五层:目标端口可达、STUN Binding 成功、网关映射已下发、Cloudflare 固定入口返回 302/307、独立外网跟随跳转后业务成功。固定域名解决的是“入口不变”,不是“第二跳自动获得隧道保护”。
留言
登录后加入讨论
你的邮箱不会公开,留言只显示昵称。