技术实践 ·

NAT 后面的软路由,怎么接入自建管理后台?Fleetd 的连接与设备批准设计

NAT 后面的 OpenWrt、iStoreOS 软路由如何接入 Fleetd?按 v0.3.0 讲清客户端主动连接、WebSocket、指纹批准、断线重连,以及删除与拒绝设备的区别。

把云服务器接进管理后台,通常比较容易理解:有一台能访问的机器,装上程序,配置地址,再看连接是否正常。

换成家里的软路由,事情就多了一层。它可能没有公网 IPv4,前面还有光猫和运营商网络。为了远程管理,再做一套端口映射、动态域名或者跳板,维护入口本身就成了工作。

在上一篇介绍 Fleetd时,我提到客户端主动连接服务端。这篇把这件事展开:连接从哪一端发起,新设备怎么获得批准,以及网络断开以后会发生什么。

下面按 Fleetd v0.3.0 的实现来讲。它解决的是把自己的服务器和软路由接进后台,不是通用的内网穿透服务,也不会把软路由后面的整个局域网自动开放出来。

先把连接方向反过来

远程管理很容易让人先想到:后台怎么找到家里的路由器?

Fleetd 的做法是让路由器上的客户端去找后台。服务端提供一个可访问的地址,客户端启动后主动建立 WebSocket 连接,监控上报和后续请求通过这条连接传递。

Linux 服务器上的 agent ──主动连接──┐
                                 ├── fleetd-server ── PostgreSQL
NAT 后的软路由 agent ──主动连接───┘          ↑
                                        管理员浏览器

这样,被管理的机器不需要为了 Fleetd 单独开放入站监听端口。服务端也不必先知道每台软路由当前的公网地址,才能向它发起连接。

但这里仍有一个前提:客户端必须能够访问服务端。 DNS 解析、出站规则、代理和网络质量都会影响连接。服务端可以通过公网域名提供服务,也可以放在客户端能访问的私有网络里;如果已有 WireGuard 等网络,也可以利用它提供的可达性。

所以,“客户端在 NAT 后面也能接入”的含义是改变了连接方向。它不会自动绕过限制出站连接的网络,更不会替你把一个双方都访问不到的服务端变成可达。

页面能打开,只说明其中一条路通了

部署时很容易遇到一种困惑:浏览器已经能登录后台,客户端却一直连不上。

这两件事检查的是不同路径。浏览器打开的是管理页面和 API;客户端需要连接配置中的 WebSocket 地址,例如:

wss://fleetd.example.com/agent/ws

这里的域名只是示例,实际应使用自己的服务端地址。反向代理除了处理普通 HTTPS 请求,还需要支持 WebSocket 升级,并避免把长连接过早关闭。

如果后台首页正常,却漏配了 WebSocket 转发,继续重装客户端通常没有帮助。应该先查客户端使用的地址、代理日志以及握手失败原因。

在 WebSocket 内,Fleetd 还使用 Noise 建立客户端与服务端之间的加密会话,客户端配置固定的服务端公钥。连接到了某个地址,不代表就接受那个地址后面的任意服务端。

两层验证各有作用:HTTPS 负责相应网络连接的证书验证,固定的服务端公钥用于确认 Noise 会话的另一端身份。公钥配置错误或迁移时意外更换了服务端身份,都可能让连接失败。

正式部署仍然应按文档使用 HTTPS。Noise 不替代浏览器访问管理页面所需要的保护,而且它的通信终点包括服务端本身,并不意味着服务端看不到收到的监控数据。

先准备客户端,再复制安装命令

上一篇的 Docker Compose 示例启动了服务端和 PostgreSQL。到这一步,后台可以打开,但它还没有自动拥有可供安装的签名客户端发布包。

准备长期使用时,需要按部署文档建立自己的发布密钥、生成并上传适合目标架构的客户端包,再使用后台提供的安装命令。v0.3.0 的公开二进制面向 Linux amd64 和 arm64;路由器型号不同,CPU 架构也可能不同,要先核对。

我的建议是,第一台接入的设备先使用 monitor 权限,完成连接和监控验证后,再决定是否需要远程操作。安装命令的形式如下:

curl -fsSL https://fleetd.example.com/install.sh | sh -s -- --permission monitor

这是示例,不要原样复制示例域名。实际命令从自己的后台获取,并在确认来源的设备上执行。安装过程需要较高权限,安装脚本的来源也属于信任的一部分;后面的签名机制不能倒过来证明最初下载的脚本可信。

安装脚本会按环境处理 systemd 或 OpenWrt 的 procd,生成客户端配置并把它安装为服务。配置涉及连接地址、服务端公钥、发布公钥、命令签名公钥以及本机权限。

这些“公钥”承担不同工作,混在一起很容易查错方向:

信息 用来回答什么问题
服务端公钥 我连接的是不是预期的 Fleetd 服务端?
客户端身份公钥及其指纹 申请接入的是哪台客户端?
命令签名公钥 这条命令是不是由我信任的签名者授权?
发布公钥 这份客户端更新包是不是由我信任的发布者签发?

连接失败和命令被拒绝,可能分别属于不同的验证过程。不是随便替换一把公钥,就能让整个流程正常。

“等待批准”是接入流程的一部分

客户端首次启动时,会加载或生成自己的身份密钥。服务端接到一个能够完成相应握手、但尚未获准的身份,会把它登记为待批准。

当前实现里,待批准阶段保留的是主机名、来源地址、公钥等登记信息,还没有进入正常监控上报和命令处理流程。因此,后台出现待批准条目后没有资源曲线,并不一定是采集程序出了问题。

管理员应该通过自己控制的安装终端或已有可信连接,读取那台机器的指纹,再与后台对照。对于已经安装完成、配置路径正确的客户端,也可以在机器上执行:

fleetd-agent -config /etc/fleetd/agent.yaml fingerprint

确认一致后再批准。不要从一条陌生的待批准记录出发,仅凭它自报的主机名就认为身份可靠。

IP 地址也只能辅助判断。多个设备可能共享同一个出口,家庭网络的地址可能变化,反向代理配置还会影响后台记录的来源地址。主机名和 IP 帮助人识别设备,真正需要核对的是预期客户端的身份。

还要区分“批准这台机器接入”和“允许它执行任意命令”。批准以后,monitor 机器仍然只监控。远程操作还受客户端本地权限和命令签名验证约束,不能把批准按钮理解为给所有设备开放 root 终端。

批准以后为什么不一定马上显示在线

在 v0.3.0 中,客户端发现自己尚未获准,会按约一分钟的固定间隔重试。管理员批准后,客户端通常要等到下一次连接,才进入正常会话并开始上报。

这不是管理员按下批准后,就在原连接上立即切换状态的流程。

如果刚批准几秒就刷新页面,暂时没有新数据,不必马上删除重装。可以结合客户端日志,确认它是否还在等待下一轮,以及下一次连接是否被接受。

一分钟也不是上线时间的服务承诺。DNS 故障、网络不可达或握手失败,都可能继续阻止接入。区分“等待下一次尝试”和“尝试了但失败”,比反复点击刷新更有用。

普通断线,走的是另一套重连逻辑

网络断开时,Fleetd 不会无间隔地重连。客户端采用指数退避并加入随机抖动,避免服务端恢复时大量机器同时冲上来。

当前代码的退避基数从 1 秒开始,逐步增加到 60 秒;实际等待还会加上随机量,所以不能简单把它写成“最长 60 秒一定重连”。若此前会话稳定运行超过一分钟,下一次断开时会把退避基数重置到较短的等待。

服务端也会发送保活消息,接收长时间没有进展时结束会话,再由客户端进入重连。这个机制处理的是连接恢复,不能据此推导所有远程操作都能自动续上。

机器重新在线,不等于断线前的命令已经成功,也不等于终端里的任务能够接着执行。 尤其是升级软件、修改网络和重启服务,操作本身就可能打断连接。重连之后应查看任务结果和实际状态,不要因为没有看到完成提示就立即重复执行。

具体的任务生命周期会在命令与终端那一篇展开;接入阶段先把“网络会话恢复”和“业务操作恢复”分开理解。

改名、换地址、重装,是三种不同的变化

客户端身份依赖密钥,不能把它与主机名或者当前 IP 绑定成同一个概念。

同一身份在新地址重新连接,和生成一把新密钥后重新登记,处理意义不同。重装时如果原身份密钥被删除,即使主机名一模一样,也可能作为新的客户端出现,需要重新核验。

反过来,把一台已经接入的系统直接克隆到其他机器,也要特别留意是否把身份密钥一起复制了。两台机器共享同一身份,会让后台无法把它们当作两个独立对象。当前服务端对同一已登记机器的新连接,会取消之前的连接,这时还可能表现成反复上下线。

因此,做系统模板时不要把已登记机器的私钥当作普通配置分发。正常重启要保留身份,批量部署新机器则要生成各自独立的身份。这两个需求不能用同一种“复制整个目录”的办法处理。

删除机器,不等于阻止它再来

Fleetd 的“删除机器”会清除后台对应的记录与历史数据。如果客户端还在运行,它再次连接时,仍可能重新出现在待批准列表里。

这通常是后台已经忘记了原来的批准关系,而设备并没有停止申请接入。

如果只是改显示名称,应使用改名功能。如果要停止管理,应按卸载文档停止并移除客户端;如果要拒绝一个身份,应使用拒绝机制。被拒绝的公钥会加入拒绝名单,之后的接入被阻止,解除拒绝后才有机会重新申请。

拒绝名单针对的是身份公钥。重新生成密钥会成为另一个身份,它依然需要审批,但不能把公钥拒绝名单理解成对物理设备永久有效的封禁。

当前实现还限制待批准机器数量为 50。这给登记队列设置了一个边界,不是完整的抗攻击方案。服务端的网络暴露范围、入口防护和管理员账户保护,仍要按自己的部署环境安排。

接入失败时,按阶段查

把流程分开后,可以先看自己到底停在哪一步:

现象 优先检查
安装后没有运行的客户端服务 架构、下载与安装错误、systemd 或 procd 服务状态
后台一直没有待批准条目 客户端地址、DNS、出站网络、WebSocket 转发与服务端公钥
已出现待批准,但没有监控数据 指纹是否核对并批准;是否仍在等待重试
批准后仍持续离线 最近一次连接日志,是否真的完成握手并进入已批准会话
机器在线,但命令被拒绝 本地权限、受信签名公钥、目标和有效期,不能只查网络
删除后又出现待批准条目 客户端是否仍运行,是否需要停止或拒绝该身份

在使用 systemd 的机器上,可以先看:

systemctl status fleetd-agent
journalctl -u fleetd-agent -n 80 --no-pager

OpenWrt / iStoreOS 使用对应的服务管理和系统日志:

/etc/init.d/fleetd-agent status
logread | grep fleetd

服务命令的输出形式可能随系统版本变化。查看日志时关注失败发生在哪个阶段,公开求助前去掉真实域名、地址和敏感内容,不要上传身份私钥。

第一台机器的目标,可以很小

我建议第一轮只验证一件完整的事:一台测试设备使用 monitor 权限启动,通过可信渠道核对指纹,批准后出现新的监控数据;再安排一次可控的客户端重启,确认它以原身份回来。

这比一上来就给所有机器 full 权限,再批量执行命令,更容易把接入问题查清楚。

Fleetd 的接入设计,围绕的是几次不同的确认:网络能不能到达,连接对象是不是预期服务端,申请接入的是不是自己的设备,以及设备允许执行什么。把它们拆开,排查和授权都会清楚一些。

下一篇继续讲浏览器签名:为什么请求已经经过后台,还要由客户端再验证一次,以及这条信任链有哪些前提和边界。