无需端口转发控制 ESP8266:一条 MQTT 出站路径
一个可从互联网访问的 broker 让家庭设备通过它自己发起的连接被控制。这对 TLS 内存、主题和重放意味着什么。
设备位于家庭路由器之后,既没有端口转发,也没有公网地址,却仍然必须在几秒内收到配置。诀窍不是防火墙上开了一个洞,而是设备向外连接到它能够到达的 broker,再由 broker 沿同一条已建立的路径把消息送回。这个设计里人们喜欢的每个特性,以及其中的每个陷阱,都来自这一次反转。
实际穿过 NAT 的是什么
当设备向 broker 打开一条 TCP 连接时,家庭路由器会建立一条映射,并在该流的整个生命周期内保持它。该流上的应答能回到设备,是因为映射已经存在。broker 从不向家里发起任何东西,因此不存在路由器需要拒绝的意外入站数据包。这就是全部的组网论据,也是该方案能在他人的网络上奏效的原因。
设备必须保活这条连接,或者重新建立它;架构定下来之后,这就成了可靠性问题。
broker 路由主题,它不代理 HTTP
MQTT 定义了发布与订阅的交换:设备订阅一个它被允许读取的主题过滤器,获授权的控制器向匹配的主题发布。broker 把发布与订阅匹配起来并投递副本。它不知道浏览器请求是什么,也不会把 HTTP GET 转发给设备的 Web 服务器。如果现有固件只会 HTTP,那么 broker 方案就需要改动应用,或者刻意加入一个翻译组件。
主题名同样不是授权。多数 broker 支持按客户端配置访问控制列表,设备应被限制在自己的发布与订阅树内,而不是共用一个全局命名空间。
设备资源决定 TLS 的形态
ESP8266 的 RAM 有限,而 TLS 握手是它做的最昂贵的事。Arduino 核心提供 BearSSL 正是为了这个限制,而在校验 CA 与固定指纹之间的选择,既改变内存占用,也改变持有其他名字的有效证书的攻击者能做什么。堆允许时,校验 broker 的 CA 是更强的选择。载荷大小同样重要:MQTT 客户端持有一个缓冲区,超过它的消息会被静默截断,除非有意调大缓冲区。
保活与套接字超时属于同一份预算,因为一个永远察觉不到连接已死的客户端会停止接收命令,却不报告错误。
会咬人的消息语义
保留消息由 broker 存储,并投递给之后匹配的订阅者,这对当前状态很好,对一次性命令却很危险:重新连接的设备可能把昨天的命令当成新的。QoS 1 保证至少投递一次,所以重复是常态,应用必须做到可重复执行,最好带上它能拒绝的标识符或时间戳。遗嘱能在设备掉线时给出状态信号,但前提是连接真的失败,而不是挂起。
干净会话的行为决定了你不在时 broker 是否保留你的订阅,而这里弄错,正是某些设备每次重连后都错过第一条消息的原因。
TLS 保护到 broker 这一段。它不是针对 broker 运营者的端到端载荷加密,运营者能读到经它发布的一切。
限定生命周期,并用无害数据测试
重连要带退避,而不是紧凑的重试循环;会话不持久时就显式重新订阅;并记录连接结果,让沉默的离线设备留下证据。用什么都不做的示例载荷测试重连路径,因为命令通道的第一次生产测试不应是真实命令。然后确认过期的保留消息在重连时会造成什么,并判断这种行为对每个主题是否可接受。
这样做,设计就去掉了一项入站网络要求,并把设备软件、broker 策略和消息语义留成看得见的工作,而不是隐藏的假设。