跳到主要内容

设计与层层实现

从电台到浏览器,六层。每一层负责什么、边界画在哪、失效时表现什么 —— 以及每条关键决策背后,当时为什么不选另一种。

分层

整条链路只有一件事在流动:把浏览器的一个动作,送到那台具体的电台上,再把结果送回来。 下面六层是这件事在物理上的六个落点。

L0
电台硬件 FT-710 / IC-7300 / IC-7300MK2。产生射频,提供 CAT/CI-V 状态。不参与任何接入逻辑 —— 它不知道有 Hub 这回事。
L1
实例本地 · MRRC Modern 跑在你自己的机器上(默认 8888)。CAT/CI-V 控制、双向音频、频谱、以及 PTT 释放的权威。
L2
隧道 你的机器主动出站 → Hub 的隧道入口 :8989 → Hub 上属于你的回环端口 188xx。客户侧零入站。
L3
Hub 入口 一条通配 vhost 服务所有实例(:9988 与 :8899,均为 TLS)。终结 TLS、按 Host 取呼号、查表得端口、按实例名校验上游证书(信任包里钉着 各实例的公钥)、透明转发数据通道。呼号自助注册是另一个 vhost。
L4
海外边缘 www.vlsc.net 的 443 路径入口。标准端口 + 真证书 + 前缀透明。定位是退化路,不是主路。
L5
客户端 浏览器 / PWA / 原生 App。负责展示与交互,不是 PTT 释放的依赖 —— 客户端失联时释放由 L1 负责。
横切
证书 · 身份 · 审计 · 遥测 贯穿所有层,不属于任何一层:通配证书(DNS-01 签发、每日自动续期)、 呼号即租户名与账号标识、访问日志只记路径不记查询串、会话并发遥测。

每层职责与边界

边界比职责更重要 —— 明确写出"这层不做什么",才能知道该在哪一层找问题。

L1 实例本地
五个数据通道
控制 · 频谱 · 收音频 · 发音频 · 天调
不做什么:不做成员与权限管理(那在 Hub,且尚未实现); 不把 PTT 释放托付给云端。

失效表现:本地页打不开;局域网就 502。
L2 隧道
frpc / frps
配置 0600,装成常驻服务,带 KeepAlive
不做什么:不终结 TLS、不做路由决策、 不知道"呼号"以外的任何业务语义。

失效表现:Hub 侧 502;日志里 start proxy success 消失。
L3 Hub 入口
通配 vhost + 注册表
一条 vhost 服务全部实例;每实例的全部差异只是注册表里的一行
不做什么:不保存会话状态(无状态,可横向扩); 永不写 PTT。

失效表现:未知呼号 → 404(不猜测别的实例); 上游证书与钉住的不符 → 502。
L4 海外边缘
443 路径代理
反代整条会话(含全部数据通道),并告知实例当前路径前缀
不做什么:不是主路。多一跳海外,音频与 PTT 延迟明显增加。

失效表现:502, 或间歇性无响应(2026-10-01 已复现,见现状一节)。

关键决策

每条决策的价值不在结论,而在当时排除了什么。

决策 内容 为什么不选另一种
AD-H01
实例主动出站
客户侧只出站,零入站 反过来的"Hub 连进来"需要在每家路由器上做端口映射 —— 家庭宽带做不到,4G/5G 更做不到。
AD-H02
通配子域
一条通配 vhost + 注册表映射 每实例一个 vhost 是上一版的做法:加一个实例就要改中心配置, 还要处理正则与前缀的优先级。规模一上来就不可维护。
AD-H03
控制面 / 数据面分离
无状态入口 ×N,有状态隧道网关 ×≥2 合成一个进程后,入口的横向扩容会牵着会话归属一起搬 —— 等于把无状态的部分也变成了有状态。
AD-H05
单写者租约
每实例同时最多一个写者 允许多写者会让两个人同时按下 PTT。这个冲突在电台侧无法仲裁 —— 电台只知道"有人在发射"。
AD-H06
PTT 权威在实例本地
Hub 只能关流通知,从不写下 PTT 依赖云端超时释放,意味着云端一断网或一延迟,电台就一直发射。 释放的权威必须和电台在同一台机器上。
AD-H07
会话凭据不进 URL
走 Cookie 与握手,不进查询串 查询串必然落进代理与实例的访问日志 —— 这个失效模式在本项目早前的形态里真实发生过,不是理论风险。
AD-H08
监听者语义
可听、可看频谱、可调频换模式、禁发射 简单做成"只读"会让人没法帮忙找台; 而调频并不改变"谁在发射"这个根本约束,所以放开它没有代价。
AD-H15
呼号即身份
租户名 = 呼号;入口 = 呼号.域名 另建一套用户名体系,会把"这是谁家的电台"变成一个要查表的问题, 而它本来是可以从地址里直接读出来的。

三条安全不变量

这三条是不能被"优化掉"的。任何实现如果违反它们,就是错的,不管跑得多快。

一 · Hub 永远不是 PTT 写者

Hub 与隧道只能发送"关流"这类通知并回收授权, 不得直接下发"切到接收"的指令。多一个写者,就会出现两条互相矛盾的释放路径, 实例侧的仲裁逻辑随之失效。

二 · 会话凭据不进 URL,也不进日志

包括 Hub、实例、以及链路中间任何代理的访问日志。 这一条同时约束前端(不拼查询串)与运维侧(日志格式不得记录查询串)。

三 · 禁止关闭上游证书校验

每一跳都校验对端证书。关掉它(哪怕"只是为了先跑通") 等于把中间人攻击变成默认配置 —— 而且这类关闭一旦上线, 通常不会有人再回头打开。

现状与目标态

这张表区分"已经跑通"和"设计好了但还没做"。两者不能混着看。

能力 状态 说明
实例出站隧道 已跑通 阶段 1 用成熟反向隧道验证通路;目标态是内置的实例代理进程
通配子域接入 + 真证书 已跑通 一条通配 vhost + 注册表;证书为真发证书,每日自动续期
数据通道透明转发 已跑通 五条通道的握手、长连接与关闭语义,与直连一致
逐跳证书校验 已开启 Hub 到实例这一跳也校验,没有一处是关闭校验换来的
会话凭据不进 URL Hub 侧已实现 Hub 侧日志格式不记查询串(已实测 0 命中);实例侧前端拼串已在 mrrc_modern feat/hub 修复(C2),未合并 main,Stable 渠道仍是旧行为 —— 现状见概览 · 能力现状表
海外边缘入口 间歇可用 2026-10-01 实测 6 次中 3 次 20 秒无响应。 已排除本地链路、www 静态服务、www 上另一条代理腿、Hub 侧 SNI/Host 组合; 问题定位在 www → Hub 这一跳,待进一步取证。
PTT 半开连接释放
(远程会话活性)
实例侧已有 隧道层待做 "TCP 还活着但对端已失联"的情形(拔网线、NAT 掉表)。 实例侧已有可开的心跳闸门(默认关闭,Hub 模式建议 3–5 s); 隧道层的主动关流还没做 —— 在此之前释放靠实例本地多层兜底,会慢一些。
自助注册 Portal(呼号核验四步) 公网入口已上线 代码在 portal/(零第三方依赖,标准库 HTTP),服务为 mrrc-portal.service,本体只绑回环; 对外经 nginx 暴露为 portal.mrrc.vlsc.net:8899 并在该层限流。授予后 gen_hub_routes.py + reload 仍由 root 手执行(有意分工)。
实例证书链(hub → 实例这一跳的一机一证) 机制就位 make_instance_cert.sh 签一张签给实例自己入口名的自签证书, 公钥钉进 trust-bundle.pem,nginx 按 $mrrc_tls_name 逐实例校验 —— 校验始终开着, 自签靠"钉住它"通过。 现网当前实例尚未迁到这条链(注册表第三列仍为旧的 radio.vlsc.net)。
实例安装器 脚本就位 deploy/install_instance_tunnel.{sh,ps1}:自取 frpc 并 校验 SHA-256、与 frps 版本 pin 死、三平台常驻。 仍是仓内脚本,无公开发布的安装包下载。
一机一证(隧道自身的设备证书) 阶段 2 隧道认证仍是 frp 的单个共享凭证 —— 这是它被定位为过渡通道的原因。 与上面那条证书链是两件事:一个是"谁能接入隧道", 一个是"这台实例是不是它声称的那台"。
独占写者租约 阶段 2 现阶段由实例侧仲裁谁在发射
成员管理与单独撤销 阶段 2 现阶段只能改监听口令,会影响所有持有它的人

边界声明

本页只讲接入层:隧道、入口、路由、证书、身份、边缘。

电台控制、音频编解码、频谱、以及 PTT 安全释放的全部细节, 权威在 MRRC Modern 自己的设计记录里 —— 本页不复制它们, 只定义与它们相邻的接口。

  • 要到产品站看 MRRC Modern 的设计与操作指南 → MRRC Modern
  • 要看本项目的完整设计记录(需求、用例、决策、风险) → mrrc_hub 仓库的 SDD/ 目录
  • 实测的运维事实(主机、端口、证书流程、排障) → SDD/12 §12.8
文档与现网不一致时以现网为准

本页所有事实来自 2026-10-01 对现网实际配置、证书与注册表的现场读取。 如果你发现页面说的和实际不符,是页面该改 —— 欢迎提 issue 指出。

← 回到 概览 · 照做走一遍 接入四步 · 遇到问题 排障
本页事实基线:2026-10-01 现场取证(现网 nginx 配置、证书有效期、注册表内容、入口可用性实测)。