技术Cloud Hub2026-10-02

云端的第一公里
—— 内网电台怎么接上去,以及它为什么曾经黑屏

一个业余无线电爱好者家里通常没有公网 IP:运营商 NAT、双 NAT、不能做端口映射、路由器不给 UPnP。 但电台想被远程用,就必须"在互联网上有一个入口"。这篇文章讲我们怎么在不要求客户改网络的前提下做到这一点, 以及在这个过程中,为什么装完之后会黑屏。

一、目标与约束

约束 后果
实例侧没有公网 IP、不能端口映射、不能 UPnP 入口不能建在客户端 ⇒ 必须**出站**建隧道
客户网络可能只放行 80/443,也可能只放行别的 隧道控制端口只能要**一个**,且要能配置
用户是业余爱好者,不是系统管理员 接入必须**在应用里点几下**完成,不能让他们敲命令
一台设备只属于一个呼号 入口名就是呼号本身:https://bg9aaa.mrrc.vlsc.net/

二、四条不变量

1. 实例主动出站,hub 从不主动连实例

实例里跑一个 frpc,拨向 tunnel.mrrc.vlsc.net:8989(唯一保留的独立端口), 把自己的 8888 映射到 hub 回环上某个独占端口(如 127.0.0.1:18803)。 hub 侧的 nginx 再把 bg9aaa.mrrc.vlsc.net 反代到那个回环端口。 客户侧因此**零入站要求**,只需能出一个 TCP。

2. hub 按"每个实例自己的证书"校验,而不是公有 CA

实例的证书是它自己签的(首启自动生成,CN/SAN 就是它的入口名),hub 端 proxy_ssl_trusted_certificate 指向一个**由注册表生成**的信任包 (系统根证书 + 各实例公钥)。这带来两个好处:续期/重签不需要跨机同步,且 只有登记过的实例会被信任。

反过来也成立:实例换了证书、hub 没重新生成信任包 ⇒ 上游校验失败 ⇒ 入口 502。 这条我们踩过两次,现在写进了运维脚本(新增/改名/换证书后跑一条命令)。

3. 路由是生成出来的,不是手写的

注册表 /etc/mrrc-hub/instances.tsv 每行是 标签 回环端口 入口名; 一条命令生成 nginx 的 map 与通配 vhost。加一个实例 = 加一行 + 重生成,**不需要碰站点配置**。

4. 门户只做非 root 的动作

呼号自助门户跑在回环上、以普通服务账号运行:它能写申请状态、能签发实例证书,但**不能** reload nginx、不能改防火墙。需要 root 的两件事(重生成路由、放行端口)由运维照抄命令完成—— 权限边界画在明处,门户被攻破也拿不到机器。

三、为什么"装完黑屏"

上线当天就有客户报:装完之后黑屏、起不来。我们在真机上复现,发现是四个 只在构建机上成立的默认值凑在一起:

默认值 客户机上发生了什么
证书路径 = 打包目录(…\_internal\certs\fullchain.pem) 普通用户对 C:\Program Files 只读 ⇒ 找不到证书 ⇒ 服务**悄悄退回纯 HTTP**,而启动器打开的是 https:// ⇒ 浏览器协议错误 ⇒ 一片黑
日志目录 = 打包目录 [WinError 5] 拒绝访问 ⇒ 一条日志都没有 ⇒ 出事时无据可查
串口 = /dev/cu.SLAB_USBtoUART(macOS 的名字) Windows 上电台明明在 COM5,程序却去找一个不存在的端口 ⇒ "电台找不到"
登录口令 = 源码里那个公开默认值 局域网里谁都能进(改了口令后没事,但首启不该是公开值)

修法不是"注意一点",而是三条结构性改动:

1. 所有"运行时要写"的默认路径 → 用户目录(%LOCALAPPDATA%\MRRC-Modern\…)
2. 缺证书 ⇒ **当场自签一张**(不再是"降级为 HTTP")
3. 加了闸门测试:任何可写默认路径落在程序目录里 ⇒ 测试直接红

   def test_runtime_paths_are_outside_the_program_directory(self): ...

第三条是这一次最值钱的产出:它把"下次注意"变成了"下次一定红"—— 同类问题再也进不了发布。顺带它当场咬出了第五个(内存通道文件)我们还没注意的默认值。

四、两个只在真机上现形的时序问题

1. 换了证书,进程还在服务旧的

TLS 上下文是进程启动时建好的。实例接入时会生成新证书并写盘,但正在运行的进程**不会**换。 hub 那边按新证书校验 ⇒ 上游失败 ⇒ 入口 502。而且它最坑的形态是顺序: 先重启应用、之后才接入 —— 这时文件变了、进程还是旧的。 最终判据改成一句事实:

证书文件的写入时间 > 本进程启动时间  ⇒  需要重启才能启用

2. 隧道是"懒启动"的

隧道原本只在打开设置对话框(调 /api/cloud/state)时才拉起—— 于是**机器重启之后,入口会一直 502,直到有人点一下设置**。这个缺口在端到端验收里被发现, 改成应用启动时就把隧道拉起来(失败只记日志,不阻塞服务)。

五、运维四条判据(都是真机换来的)

① 别用退出码证明成功。构建脚本退出码 0、"Successful compile"、version.txt 写着新版本——这三样都可能同时成立而包是旧的。可靠组合是:版本 + 今天的时间戳 + 与上一版不同的哈希,最后再加一步"装完真跑"。

② 同一个版本号不能重建。升级通道比的是版本串:用同一个号重新构建,装了那一版的人永远拿不到新包,点"升级"只会反复重放同一版本。

③ 发版列车只能有一个司机。两个写者并行时,清单文件会被覆盖成"新标签 + 旧数字",更新器拿到对不上号的哈希。

④ 每次真机失败,当天变成一条测试或一条不变量。否则它会换个名字再来。

六、对使用者:三步接入

① 安装 MRRC Modern(Windows 安装包 / macOS DMG)
② 菜单(顶栏 ☰)→ 接入云端 → 填呼号 → 申请
③ 运维在门户核验并批准(或运营方给你一个登记口令,填进框里即可)
   就这三步:批准一到,应用自己接上 —— 服务端每 30 秒自查一次(v1.25.0),你不需要再点任何东西

完成后你的入口是:  https://<呼号>.mrrc.vlsc.net/
   —— 就是呼号本身,没有端口、没有路径、没有产品后缀

出问题时:界面里点 🐞 一键带日志上报;环境乱掉了可以从站点下载 一键清理脚本,它先把你配置与证书备份到桌面,再彻底清干净重来(录音不进备份、但会被一起删除,脚本开始前会再提醒一次;细节见指南 §6.2)。

启动器万一打不开(黑屏/闪退),服务本身照样能跑:开始菜单里的 MRRC Modern Server + 浏览器打开 https://127.0.0.1:8888/(自签名证书会提示一次,继续即可),口令会打印在窗口里。详细步骤见指南 §6.1。

写于 2026-10-02,MRRC Cloud Hub 上线首日。文中的每个细节都来自当天的真机日志、提交与产物校验。
另见随笔:阳春白雪与下里巴人 —— 论肌肉与思维